aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/stale.yml24
-rw-r--r--.gitignore3
-rw-r--r--.rubocop.yml7
-rw-r--r--.travis.yml72
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--Gemfile23
-rw-r--r--Gemfile.lock275
-rw-r--r--MIT-LICENSE2
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md6
-rw-r--r--RELEASING_RAILS.md18
-rw-r--r--actioncable/CHANGELOG.md38
-rw-r--r--actioncable/MIT-LICENSE2
-rw-r--r--actioncable/README.md15
-rw-r--r--actioncable/Rakefile36
-rw-r--r--actioncable/actioncable.gemspec2
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection.coffee2
-rwxr-xr-xactioncable/bin/test4
-rw-r--r--actioncable/lib/action_cable.rb2
-rw-r--r--actioncable/lib/action_cable/channel/base.rb29
-rw-r--r--actioncable/lib/action_cable/connection/authorization.rb10
-rw-r--r--actioncable/lib/action_cable/connection/base.rb21
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb4
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb2
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb4
-rw-r--r--actioncable/lib/action_cable/connection/tagged_logger_proxy.rb4
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb2
-rw-r--r--actioncable/lib/action_cable/engine.rb2
-rw-r--r--actioncable/lib/action_cable/gem_version.rb2
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb2
-rw-r--r--actioncable/lib/action_cable/subscription_adapter.rb1
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb26
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/evented_redis.rb12
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb2
-rw-r--r--actioncable/lib/rails/generators/channel/USAGE4
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb6
-rw-r--r--actioncable/package.json2
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb9
-rw-r--r--actioncable/test/channel/stream_test.rb4
-rw-r--r--actioncable/test/connection/identifier_test.rb2
-rw-r--r--actioncable/test/connection/multiple_identifiers_test.rb2
-rw-r--r--actioncable/test/connection/string_identifier_test.rb2
-rw-r--r--actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee10
-rw-r--r--actioncable/test/javascript/src/test_helpers/index.coffee3
-rw-r--r--actioncable/test/javascript/src/unit/consumer_test.coffee7
-rw-r--r--actioncable/test/javascript/src/unit/subscription_test.coffee40
-rw-r--r--actioncable/test/javascript/src/unit/subscriptions_test.coffee25
-rw-r--r--actioncable/test/subscription_adapter/channel_prefix.rb36
-rw-r--r--actioncable/test/subscription_adapter/evented_redis_test.rb32
-rw-r--r--actioncable/test/subscription_adapter/redis_test.rb2
-rw-r--r--actionmailer/CHANGELOG.md7
-rw-r--r--actionmailer/MIT-LICENSE2
-rwxr-xr-xactionmailer/bin/test2
-rw-r--r--actionmailer/lib/action_mailer.rb3
-rw-r--r--actionmailer/lib/action_mailer/base.rb70
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb2
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb2
-rw-r--r--actionmailer/lib/action_mailer/inline_preview_interceptor.rb6
-rw-r--r--actionmailer/lib/action_mailer/parameterized.rb152
-rw-r--r--actionmailer/lib/action_mailer/preview.rb12
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb20
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb4
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb7
-rw-r--r--actionmailer/test/abstract_unit.rb22
-rw-r--r--actionmailer/test/base_test.rb26
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb6
-rw-r--r--actionmailer/test/mail_helper_test.rb12
-rw-r--r--actionmailer/test/mailers/params_mailer.rb11
-rw-r--r--actionmailer/test/message_delivery_test.rb6
-rw-r--r--actionmailer/test/parameterized_test.rb55
-rw-r--r--actionmailer/test/test_helper_test.rb18
-rw-r--r--actionpack/CHANGELOG.md298
-rw-r--r--actionpack/MIT-LICENSE2
-rwxr-xr-xactionpack/bin/test2
-rw-r--r--actionpack/lib/abstract_controller/base.rb2
-rw-r--r--actionpack/lib/abstract_controller/caching.rb4
-rw-r--r--actionpack/lib/abstract_controller/collector.rb2
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb8
-rw-r--r--actionpack/lib/action_controller/api.rb7
-rw-r--r--actionpack/lib/action_controller/base.rb8
-rw-r--r--actionpack/lib/action_controller/caching.rb2
-rw-r--r--actionpack/lib/action_controller/metal.rb21
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb9
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_flash.rb4
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb2
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb16
-rw-r--r--actionpack/lib/action_controller/metal/head.rb2
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb6
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb8
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb6
-rw-r--r--actionpack/lib/action_controller/metal/live.rb8
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb4
-rw-r--r--actionpack/lib/action_controller/metal/parameter_encoding.rb39
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb16
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb11
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb2
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb18
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb56
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb2
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb8
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb114
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb2
-rw-r--r--actionpack/lib/action_controller/railtie.rb2
-rw-r--r--actionpack/lib/action_controller/renderer.rb10
-rw-r--r--actionpack/lib/action_controller/test_case.rb15
-rw-r--r--actionpack/lib/action_dispatch.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb10
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb12
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb25
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb34
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb19
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb12
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb10
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb1
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb377
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y5
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb7
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb30
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb7
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb30
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb12
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb12
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb5
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb50
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb17
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb2
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb8
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb14
-rw-r--r--actionpack/lib/action_dispatch/routing.rb4
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb250
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb36
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb14
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb136
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb18
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb126
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb34
-rw-r--r--actionpack/lib/action_dispatch/system_testing/server.rb32
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb94
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb20
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb15
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb44
-rw-r--r--actionpack/lib/action_dispatch/testing/request_encoder.rb31
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb36
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb2
-rw-r--r--actionpack/lib/action_pack.rb2
-rw-r--r--actionpack/lib/action_pack/gem_version.rb2
-rw-r--r--actionpack/test/abstract_unit.rb36
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb24
-rw-r--r--actionpack/test/controller/api/with_helpers_test.rb26
-rw-r--r--actionpack/test/controller/filters_test.rb12
-rw-r--r--actionpack/test/controller/flash_hash_test.rb8
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb3
-rw-r--r--actionpack/test/controller/integration_test.rb39
-rw-r--r--actionpack/test/controller/live_stream_test.rb1
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb2
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb4
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb2
-rw-r--r--actionpack/test/controller/new_base/base_test.rb2
-rw-r--r--actionpack/test/controller/new_base/render_context_test.rb16
-rw-r--r--actionpack/test/controller/new_base/render_streaming_test.rb4
-rw-r--r--actionpack/test/controller/parameter_encoding_test.rb39
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb63
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_permit_test.rb5
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb50
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb17
-rw-r--r--actionpack/test/controller/redirect_test.rb6
-rw-r--r--actionpack/test/controller/render_test.rb1
-rw-r--r--actionpack/test/controller/renderer_test.rb24
-rw-r--r--actionpack/test/controller/request/test_request_test.rb26
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb2
-rw-r--r--actionpack/test/controller/rescue_test.rb4
-rw-r--r--actionpack/test/controller/resources_test.rb2
-rw-r--r--actionpack/test/controller/send_file_test.rb9
-rw-r--r--actionpack/test/controller/test_case_test.rb36
-rw-r--r--actionpack/test/controller/url_for_test.rb23
-rw-r--r--actionpack/test/dispatch/callbacks_test.rb18
-rw-r--r--actionpack/test/dispatch/cookies_test.rb18
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb86
-rw-r--r--actionpack/test/dispatch/reloader_test.rb128
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb2
-rw-r--r--actionpack/test/dispatch/request_test.rb32
-rw-r--r--actionpack/test/dispatch/response_test.rb7
-rw-r--r--actionpack/test/dispatch/routing/custom_url_helpers_test.rb325
-rw-r--r--actionpack/test/dispatch/routing/ipv6_redirect_test.rb2
-rw-r--r--actionpack/test/dispatch/routing_test.rb114
-rw-r--r--actionpack/test/dispatch/runner_test.rb1
-rw-r--r--actionpack/test/dispatch/session/cache_store_test.rb6
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb8
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb2
-rw-r--r--actionpack/test/dispatch/ssl_test.rb8
-rw-r--r--actionpack/test/dispatch/static_test.rb12
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb21
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb41
-rw-r--r--actionpack/test/dispatch/system_testing/server_test.rb17
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb13
-rw-r--r--actionpack/test/dispatch/test_request_test.rb2
-rw-r--r--actionpack/test/journey/path/pattern_test.rb6
-rw-r--r--actionpack/test/journey/route_test.rb2
-rw-r--r--actionpack/test/lib/controller/fake_models.rb4
-rw-r--r--actionview/.gitignore2
-rw-r--r--actionview/CHANGELOG.md121
-rw-r--r--actionview/MIT-LICENSE2
-rw-r--r--actionview/RUNNING_UJS_TESTS.rdoc7
-rw-r--r--actionview/Rakefile44
-rw-r--r--actionview/actionview.gemspec4
-rw-r--r--actionview/app/assets/javascripts/MIT-LICENSE20
-rw-r--r--actionview/app/assets/javascripts/README.md49
-rw-r--r--actionview/app/assets/javascripts/config.coffee34
-rw-r--r--actionview/app/assets/javascripts/features/confirm.coffee26
-rw-r--r--actionview/app/assets/javascripts/features/disable.coffee82
-rw-r--r--actionview/app/assets/javascripts/features/method.coffee34
-rw-r--r--actionview/app/assets/javascripts/features/remote.coffee90
-rw-r--r--actionview/app/assets/javascripts/rails-ujs.coffee80
-rw-r--r--actionview/app/assets/javascripts/utils/ajax.coffee95
-rw-r--r--actionview/app/assets/javascripts/utils/csrf.coffee25
-rw-r--r--actionview/app/assets/javascripts/utils/dom.coffee28
-rw-r--r--actionview/app/assets/javascripts/utils/event.coffee40
-rw-r--r--actionview/app/assets/javascripts/utils/form.coffee36
-rwxr-xr-xactionview/bin/test2
-rw-r--r--actionview/blade.yml11
-rw-r--r--actionview/coffeelint.json135
-rw-r--r--actionview/lib/action_view.rb2
-rw-r--r--actionview/lib/action_view/base.rb2
-rw-r--r--actionview/lib/action_view/digestor.rb6
-rw-r--r--actionview/lib/action_view/gem_version.rb2
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb29
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb385
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb34
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb24
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb46
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb12
-rw-r--r--actionview/lib/action_view/helpers/tags/date_select.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/translator.rb2
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb22
-rw-r--r--actionview/lib/action_view/layouts.rb4
-rw-r--r--actionview/lib/action_view/log_subscriber.rb12
-rw-r--r--actionview/lib/action_view/lookup_context.rb16
-rw-r--r--actionview/lib/action_view/railtie.rb4
-rw-r--r--actionview/lib/action_view/record_identifier.rb4
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb8
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb10
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb4
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb4
-rw-r--r--actionview/lib/action_view/rendering.rb10
-rw-r--r--actionview/lib/action_view/routing_url_for.rb19
-rw-r--r--actionview/lib/action_view/template.rb27
-rw-r--r--actionview/lib/action_view/template/handlers/builder.rb6
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb77
-rw-r--r--actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb9
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb81
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubis.rb81
-rw-r--r--actionview/lib/action_view/template/resolver.rb12
-rw-r--r--actionview/lib/action_view/template/text.rb7
-rw-r--r--actionview/lib/action_view/test_case.rb12
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb50
-rw-r--r--actionview/lib/action_view/view_paths.rb2
-rw-r--r--actionview/package.json36
-rw-r--r--actionview/test/abstract_unit.rb24
-rw-r--r--actionview/test/actionpack/abstract/helper_test.rb2
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb2
-rw-r--r--actionview/test/actionpack/controller/render_test.rb38
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb4
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb57
-rw-r--r--actionview/test/activerecord/render_partial_with_record_identification_test.rb2
-rw-r--r--actionview/test/fixtures/test/_partial_iteration_1.erb1
-rw-r--r--actionview/test/fixtures/test/_partial_iteration_2.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_instance_variable.erb1
-rw-r--r--actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb1
-rw-r--r--actionview/test/lib/controller/fake_models.rb25
-rw-r--r--actionview/test/template/active_model_helper_test.rb2
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb2
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb2
-rw-r--r--actionview/test/template/capture_helper_test.rb14
-rw-r--r--actionview/test/template/compiled_templates_test.rb14
-rw-r--r--actionview/test/template/date_helper_test.rb3
-rw-r--r--actionview/test/template/digestor_test.rb4
-rw-r--r--actionview/test/template/erb/deprecated_erubis_implementation_test.rb13
-rw-r--r--actionview/test/template/erb/helper.rb2
-rw-r--r--actionview/test/template/erb/tag_helper_test.rb4
-rw-r--r--actionview/test/template/erb_util_test.rb7
-rw-r--r--actionview/test/template/form_collections_helper_test.rb3
-rw-r--r--actionview/test/template/form_helper/form_with_test.rb2231
-rw-r--r--actionview/test/template/form_helper_test.rb433
-rw-r--r--actionview/test/template/form_options_helper_test.rb236
-rw-r--r--actionview/test/template/form_tag_helper_test.rb17
-rw-r--r--actionview/test/template/number_helper_test.rb14
-rw-r--r--actionview/test/template/output_safety_helper_test.rb27
-rw-r--r--actionview/test/template/render_test.rb35
-rw-r--r--actionview/test/template/sanitize_helper_test.rb2
-rw-r--r--actionview/test/template/test_case_test.rb2
-rw-r--r--actionview/test/template/text_test.rb22
-rw-r--r--actionview/test/template/url_helper_test.rb17
-rw-r--r--actionview/test/ujs/.gitignore1
-rw-r--r--actionview/test/ujs/config.ru4
-rw-r--r--actionview/test/ujs/public/test/.eslintrc.yml21
-rw-r--r--actionview/test/ujs/public/test/call-remote-callbacks.js273
-rw-r--r--actionview/test/ujs/public/test/call-remote.js247
-rw-r--r--actionview/test/ujs/public/test/csrf-refresh.js24
-rw-r--r--actionview/test/ujs/public/test/csrf-token.js27
-rw-r--r--actionview/test/ujs/public/test/data-confirm.js316
-rw-r--r--actionview/test/ujs/public/test/data-disable-with.js391
-rw-r--r--actionview/test/ujs/public/test/data-disable.js321
-rw-r--r--actionview/test/ujs/public/test/data-method.js85
-rw-r--r--actionview/test/ujs/public/test/data-remote.js415
-rw-r--r--actionview/test/ujs/public/test/override.js56
-rw-r--r--actionview/test/ujs/public/test/settings.js116
-rw-r--r--actionview/test/ujs/public/vendor/jquery.metadata.js122
-rw-r--r--actionview/test/ujs/public/vendor/qunit.css237
-rw-r--r--actionview/test/ujs/public/vendor/qunit.js2288
-rw-r--r--actionview/test/ujs/server.rb72
-rw-r--r--actionview/test/ujs/views/layouts/application.html.erb25
-rw-r--r--actionview/test/ujs/views/tests/index.html.erb11
-rw-r--r--activejob/CHANGELOG.md42
-rw-r--r--activejob/MIT-LICENSE2
-rw-r--r--activejob/Rakefile5
-rwxr-xr-xactivejob/bin/test4
-rw-r--r--activejob/lib/active_job.rb2
-rw-r--r--activejob/lib/active_job/enqueuing.rb4
-rw-r--r--activejob/lib/active_job/gem_version.rb2
-rw-r--r--activejob/lib/active_job/logging.rb4
-rw-r--r--activejob/lib/active_job/queue_adapter.rb1
-rw-r--r--activejob/lib/active_job/queue_adapters/test_adapter.rb1
-rw-r--r--activejob/lib/active_job/test_helper.rb89
-rw-r--r--activejob/lib/rails/generators/job/job_generator.rb2
-rw-r--r--activejob/test/cases/logging_test.rb12
-rw-r--r--activejob/test/cases/queue_adapter_test.rb1
-rw-r--r--activejob/test/cases/queue_naming_test.rb2
-rw-r--r--activejob/test/cases/queue_priority_test.rb4
-rw-r--r--activejob/test/cases/test_helper_test.rb42
-rw-r--r--activejob/test/helper.rb2
-rw-r--r--activejob/test/jobs/application_job.rb2
-rw-r--r--activejob/test/jobs/queue_adapter_job.rb3
-rw-r--r--activejob/test/support/integration/adapters/backburner.rb4
-rw-r--r--activejob/test/support/integration/adapters/sneakers.rb2
-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.rb2
-rw-r--r--activejob/test/support/sneakers/inline.rb2
-rw-r--r--activemodel/CHANGELOG.md36
-rw-r--r--activemodel/MIT-LICENSE2
-rwxr-xr-xactivemodel/bin/test2
-rw-r--r--activemodel/lib/active_model.rb3
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb18
-rw-r--r--activemodel/lib/active_model/callbacks.rb7
-rw-r--r--activemodel/lib/active_model/errors.rb35
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb2
-rw-r--r--activemodel/lib/active_model/gem_version.rb2
-rw-r--r--activemodel/lib/active_model/model.rb4
-rw-r--r--activemodel/lib/active_model/naming.rb2
-rw-r--r--activemodel/lib/active_model/test_case.rb4
-rw-r--r--activemodel/lib/active_model/translation.rb2
-rw-r--r--activemodel/lib/active_model/type.rb14
-rw-r--r--activemodel/lib/active_model/type/date.rb2
-rw-r--r--activemodel/lib/active_model/type/decimal.rb11
-rw-r--r--activemodel/lib/active_model/type/decimal_without_scale.rb11
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb8
-rw-r--r--activemodel/lib/active_model/type/integer.rb4
-rw-r--r--activemodel/lib/active_model/type/registry.rb4
-rw-r--r--activemodel/lib/active_model/type/text.rb11
-rw-r--r--activemodel/lib/active_model/validations.rb6
-rw-r--r--activemodel/lib/active_model/validations/absence.rb2
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb4
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb5
-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/format.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb2
-rw-r--r--activemodel/lib/active_model/validations/length.rb4
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb15
-rw-r--r--activemodel/lib/active_model/validations/presence.rb2
-rw-r--r--activemodel/lib/active_model/validations/validates.rb6
-rw-r--r--activemodel/lib/active_model/validations/with.rb2
-rw-r--r--activemodel/lib/active_model/validator.rb4
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb2
-rw-r--r--activemodel/test/cases/callbacks_test.rb11
-rw-r--r--activemodel/test/cases/errors_test.rb49
-rw-r--r--activemodel/test/cases/helper.rb20
-rw-r--r--activemodel/test/cases/secure_password_test.rb2
-rw-r--r--activemodel/test/cases/type/binary_test.rb2
-rw-r--r--activemodel/test/cases/type/date_test.rb8
-rw-r--r--activemodel/test/cases/type/date_time_test.rb8
-rw-r--r--activemodel/test/cases/type/decimal_test.rb8
-rw-r--r--activemodel/test/cases/type/float_test.rb8
-rw-r--r--activemodel/test/cases/type/integer_test.rb1
-rw-r--r--activemodel/test/cases/type/time_test.rb6
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb14
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb20
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb9
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb20
-rw-r--r--activerecord/CHANGELOG.md306
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/Rakefile5
-rw-r--r--activerecord/activerecord.gemspec2
-rwxr-xr-xactiverecord/bin/test2
-rw-r--r--activerecord/lib/active_record.rb5
-rw-r--r--activerecord/lib/active_record/aggregations.rb4
-rw-r--r--activerecord/lib/active_record/associations.rb58
-rw-r--r--activerecord/lib/active_record/associations/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb29
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb9
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb68
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb48
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb14
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb4
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb33
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb4
-rw-r--r--activerecord/lib/active_record/attribute.rb19
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb2
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb2
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb22
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb21
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb35
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb11
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb27
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb10
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb11
-rw-r--r--activerecord/lib/active_record/attribute_set.rb2
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb2
-rw-r--r--activerecord/lib/active_record/attribute_set/yaml_encoder.rb2
-rw-r--r--activerecord/lib/active_record/attributes.rb2
-rw-r--r--activerecord/lib/active_record/autosave_association.rb10
-rw-r--r--activerecord/lib/active_record/callbacks.rb55
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb78
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb154
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb95
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb181
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/column.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb47
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb204
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb145
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb155
-rw-r--r--activerecord/lib/active_record/core.rb39
-rw-r--r--activerecord/lib/active_record/counter_cache.rb68
-rw-r--r--activerecord/lib/active_record/enum.rb2
-rw-r--r--activerecord/lib/active_record/errors.rb48
-rw-r--r--activerecord/lib/active_record/explain.rb1
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb7
-rw-r--r--activerecord/lib/active_record/fixtures.rb25
-rw-r--r--activerecord/lib/active_record/gem_version.rb2
-rw-r--r--activerecord/lib/active_record/inheritance.rb17
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb2
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb12
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb11
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb20
-rw-r--r--activerecord/lib/active_record/migration.rb74
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb93
-rw-r--r--activerecord/lib/active_record/model_schema.rb1
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb6
-rw-r--r--activerecord/lib/active_record/null_relation.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb48
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/railtie.rb10
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb4
-rw-r--r--activerecord/lib/active_record/railties/databases.rake68
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb2
-rw-r--r--activerecord/lib/active_record/reflection.rb204
-rw-r--r--activerecord/lib/active_record/relation.rb64
-rw-r--r--activerecord/lib/active_record/relation/batches.rb2
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb19
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb12
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb48
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb25
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb5
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/class_handler.rb27
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb39
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb42
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb48
-rw-r--r--activerecord/lib/active_record/result.rb13
-rw-r--r--activerecord/lib/active_record/sanitization.rb29
-rw-r--r--activerecord/lib/active_record/schema.rb4
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb4
-rw-r--r--activerecord/lib/active_record/schema_migration.rb8
-rw-r--r--activerecord/lib/active_record/scoping/default.rb12
-rw-r--r--activerecord/lib/active_record/scoping/named.rb12
-rw-r--r--activerecord/lib/active_record/secure_token.rb2
-rw-r--r--activerecord/lib/active_record/serialization.rb2
-rw-r--r--activerecord/lib/active_record/store.rb13
-rw-r--r--activerecord/lib/active_record/table_metadata.rb4
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb42
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb6
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb29
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb12
-rw-r--r--activerecord/lib/active_record/timestamp.rb58
-rw-r--r--activerecord/lib/active_record/transactions.rb40
-rw-r--r--activerecord/lib/active_record/type.rb6
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb4
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb13
-rw-r--r--activerecord/lib/active_record/type/serialized.rb4
-rw-r--r--activerecord/lib/active_record/type/text.rb9
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb (renamed from activemodel/lib/active_model/type/unsigned_integer.rb)4
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb2
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb4
-rw-r--r--activerecord/lib/active_record/validations/presence.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb45
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb8
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb7
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb6
-rw-r--r--activerecord/test/assets/schema_dump_5_1.yml345
-rw-r--r--activerecord/test/cases/adapter_test.rb146
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb7
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb37
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb23
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/sql_types_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/transaction_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb5
-rw-r--r--activerecord/test/cases/adapters/mysql2/virtual_column_test.rb59
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb25
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb19
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/numbers_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb24
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb175
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb80
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb9
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb51
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb36
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb1
-rw-r--r--activerecord/test/cases/ar_schema_test.rb217
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb52
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb2
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb257
-rw-r--r--activerecord/test/cases/associations/eager_test.rb35
-rw-r--r--activerecord/test/cases/associations/extension_test.rb5
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb16
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb56
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb57
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb56
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb15
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb2
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb2
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb2
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb2
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb2
-rw-r--r--activerecord/test/cases/associations/required_test.rb42
-rw-r--r--activerecord/test/cases/associations_test.rb30
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb2
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb32
-rw-r--r--activerecord/test/cases/attribute_set_test.rb2
-rw-r--r--activerecord/test/cases/attributes_test.rb8
-rw-r--r--activerecord/test/cases/autosave_association_test.rb40
-rw-r--r--activerecord/test/cases/base_test.rb38
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb121
-rw-r--r--activerecord/test/cases/calculations_test.rb27
-rw-r--r--activerecord/test/cases/callbacks_test.rb165
-rw-r--r--activerecord/test/cases/coders/yaml_column_test.rb24
-rw-r--r--activerecord/test/cases/column_definition_test.rb58
-rw-r--r--activerecord/test/cases/comment_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb95
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb50
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb54
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb20
-rw-r--r--activerecord/test/cases/connection_pool_test.rb113
-rw-r--r--activerecord/test/cases/counter_cache_test.rb151
-rw-r--r--activerecord/test/cases/database_statements_test.rb6
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb2
-rw-r--r--activerecord/test/cases/date_time_test.rb2
-rw-r--r--activerecord/test/cases/defaults_test.rb14
-rw-r--r--activerecord/test/cases/dirty_test.rb32
-rw-r--r--activerecord/test/cases/errors_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb50
-rw-r--r--activerecord/test/cases/fixtures_test.rb133
-rw-r--r--activerecord/test/cases/helper.rb4
-rw-r--r--activerecord/test/cases/inheritance_test.rb14
-rw-r--r--activerecord/test/cases/integration_test.rb4
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb54
-rw-r--r--activerecord/test/cases/json_serialization_test.rb15
-rw-r--r--activerecord/test/cases/locking_test.rb43
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb24
-rw-r--r--activerecord/test/cases/migration/columns_test.rb10
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb125
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb31
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb67
-rw-r--r--activerecord/test/cases/migration/index_test.rb16
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb12
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb55
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb7
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb4
-rw-r--r--activerecord/test/cases/migration_test.rb40
-rw-r--r--activerecord/test/cases/migrator_test.rb89
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb4
-rw-r--r--activerecord/test/cases/persistence_test.rb38
-rw-r--r--activerecord/test/cases/primary_keys_test.rb190
-rw-r--r--activerecord/test/cases/query_cache_test.rb235
-rw-r--r--activerecord/test/cases/quoting_test.rb107
-rw-r--r--activerecord/test/cases/readonly_test.rb2
-rw-r--r--activerecord/test/cases/reflection_test.rb47
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb3
-rw-r--r--activerecord/test/cases/relation/merging_test.rb4
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb22
-rw-r--r--activerecord/test/cases/relation/or_test.rb2
-rw-r--r--activerecord/test/cases/relation/where_test.rb2
-rw-r--r--activerecord/test/cases/relation_test.rb4
-rw-r--r--activerecord/test/cases/relations_test.rb108
-rw-r--r--activerecord/test/cases/sanitize_test.rb10
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb69
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb68
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb20
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb6
-rw-r--r--activerecord/test/cases/secure_token_test.rb2
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb20
-rw-r--r--activerecord/test/cases/statement_cache_test.rb26
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb26
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb44
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb64
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb6
-rw-r--r--activerecord/test/cases/test_fixtures_test.rb20
-rw-r--r--activerecord/test/cases/time_precision_test.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb23
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb85
-rw-r--r--activerecord/test/cases/transactions_test.rb18
-rw-r--r--activerecord/test/cases/type/unsigned_integer_test.rb (renamed from activemodel/test/cases/type/unsigned_integer_test.rb)5
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb22
-rw-r--r--activerecord/test/cases/view_test.rb12
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb4
-rw-r--r--activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml3
-rw-r--r--activerecord/test/fixtures/other_dogs.yml2
-rw-r--r--activerecord/test/fixtures/subscribers.yml2
-rw-r--r--activerecord/test/models/boolean.rb3
-rw-r--r--activerecord/test/models/company.rb4
-rw-r--r--activerecord/test/models/company_in_module.rb2
-rw-r--r--activerecord/test/models/customer.rb2
-rw-r--r--activerecord/test/models/developer.rb8
-rw-r--r--activerecord/test/models/family.rb4
-rw-r--r--activerecord/test/models/family_tree.rb4
-rw-r--r--activerecord/test/models/non_primary_key.rb2
-rw-r--r--activerecord/test/models/other_dog.rb5
-rw-r--r--activerecord/test/models/parrot.rb5
-rw-r--r--activerecord/test/models/post.rb4
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--activerecord/test/models/subject.rb2
-rw-r--r--activerecord/test/models/topic.rb2
-rw-r--r--activerecord/test/models/user.rb6
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb5
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb34
-rw-r--r--activerecord/test/schema/schema.rb45
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb18
-rw-r--r--activerecord/test/support/connection.rb6
-rw-r--r--activesupport/CHANGELOG.md329
-rw-r--r--activesupport/MIT-LICENSE2
-rwxr-xr-xactivesupport/bin/generate_tables4
-rwxr-xr-xactivesupport/bin/test2
-rw-r--r--activesupport/lib/active_support.rb10
-rw-r--r--activesupport/lib/active_support/array_inquirer.rb4
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb2
-rw-r--r--activesupport/lib/active_support/cache.rb20
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb5
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb14
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb14
-rw-r--r--activesupport/lib/active_support/cache/null_store.rb8
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb12
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb8
-rw-r--r--activesupport/lib/active_support/callbacks.rb78
-rw-r--r--activesupport/lib/active_support/core_ext.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/class/subclasses.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb18
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/compatibility.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/integer/time.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb126
-rw-r--r--activesupport/lib/active_support/core_ext/object/with_options.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb18
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/time/compatibility.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb3
-rw-r--r--activesupport/lib/active_support/dependencies.rb2
-rw-r--r--activesupport/lib/active_support/deprecation.rb3
-rw-r--r--activesupport/lib/active_support/deprecation/constant_accessor.rb50
-rw-r--r--activesupport/lib/active_support/deprecation/method_wrappers.rb2
-rw-r--r--activesupport/lib/active_support/deprecation/proxy_wrappers.rb5
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb6
-rw-r--r--activesupport/lib/active_support/duration.rb230
-rw-r--r--activesupport/lib/active_support/duration/iso8601_serializer.rb2
-rw-r--r--activesupport/lib/active_support/evented_file_update_checker.rb11
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb4
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb4
-rw-r--r--activesupport/lib/active_support/gem_version.rb2
-rw-r--r--activesupport/lib/active_support/gzip.rb2
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb14
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb4
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb8
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb14
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb2
-rw-r--r--activesupport/lib/active_support/log_subscriber.rb4
-rw-r--r--activesupport/lib/active_support/logger.rb4
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb15
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb15
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb28
-rw-r--r--activesupport/lib/active_support/notifications.rb4
-rw-r--r--activesupport/lib/active_support/number_helper.rb4
-rw-r--r--activesupport/lib/active_support/number_helper/number_converter.rb10
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb4
-rw-r--r--activesupport/lib/active_support/rescuable.rb2
-rw-r--r--activesupport/lib/active_support/string_inquirer.rb2
-rw-r--r--activesupport/lib/active_support/subscriber.rb7
-rw-r--r--activesupport/lib/active_support/testing/autorun.rb4
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb17
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb10
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb15
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb82
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin1068675 -> 1116857 bytes
-rw-r--r--activesupport/lib/active_support/xml_mini.rb18
-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/nokogirisax.rb2
-rw-r--r--activesupport/test/abstract_unit.rb24
-rw-r--r--activesupport/test/array_inquirer_test.rb20
-rw-r--r--activesupport/test/autoloading_fixtures/prepend.rb8
-rw-r--r--activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb2
-rw-r--r--activesupport/test/broadcast_logger_test.rb22
-rw-r--r--activesupport/test/caching_test.rb23
-rw-r--r--activesupport/test/callbacks_test.rb172
-rw-r--r--activesupport/test/class_cache_test.rb2
-rw-r--r--activesupport/test/concern_test.rb2
-rw-r--r--activesupport/test/constantize_test_cases.rb5
-rw-r--r--activesupport/test/core_ext/class_test.rb10
-rw-r--r--activesupport/test/core_ext/date_and_time_compatibility_test.rb166
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb4
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb35
-rw-r--r--activesupport/test/core_ext/duration_test.rb212
-rw-r--r--activesupport/test/core_ext/enumerable_test.rb2
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb112
-rw-r--r--activesupport/test/core_ext/kernel_test.rb16
-rw-r--r--activesupport/test/core_ext/marshal_test.rb13
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb6
-rw-r--r--activesupport/test/core_ext/module/attribute_aliasing_test.rb4
-rw-r--r--activesupport/test/core_ext/module/introspection_test.rb37
-rw-r--r--activesupport/test/core_ext/module_test.rb54
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb14
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb4
-rw-r--r--activesupport/test/core_ext/object/deep_dup_test.rb8
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb18
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb23
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb38
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb46
-rw-r--r--activesupport/test/dependencies_test.rb27
-rw-r--r--activesupport/test/deprecation_test.rb33
-rw-r--r--activesupport/test/descendants_tracker_test_cases.rb2
-rw-r--r--activesupport/test/evented_file_update_checker_test.rb8
-rw-r--r--activesupport/test/executor_test.rb2
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb6
-rw-r--r--activesupport/test/gzip_test.rb10
-rw-r--r--activesupport/test/inflector_test.rb44
-rw-r--r--activesupport/test/inflector_test_cases.rb1
-rw-r--r--activesupport/test/json/decoding_test.rb9
-rw-r--r--activesupport/test/json/encoding_test.rb4
-rw-r--r--activesupport/test/json/encoding_test_cases.rb6
-rw-r--r--activesupport/test/message_verifier_test.rb2
-rw-r--r--activesupport/test/multibyte_chars_test.rb19
-rw-r--r--activesupport/test/multibyte_conformance_test.rb2
-rw-r--r--activesupport/test/multibyte_grapheme_break_conformance_test.rb2
-rw-r--r--activesupport/test/multibyte_normalization_conformance_test.rb2
-rw-r--r--activesupport/test/multibyte_test_helpers.rb2
-rw-r--r--activesupport/test/notifications_test.rb2
-rw-r--r--activesupport/test/number_helper_i18n_test.rb1
-rw-r--r--activesupport/test/reloader_test.rb14
-rw-r--r--activesupport/test/safe_buffer_test.rb2
-rw-r--r--activesupport/test/security_utils_test.rb7
-rw-r--r--activesupport/test/string_inquirer_test.rb21
-rw-r--r--activesupport/test/test_case_test.rb4
-rw-r--r--activesupport/test/time_travel_test.rb22
-rw-r--r--activesupport/test/time_zone_test.rb188
-rw-r--r--activesupport/test/transliterate_test.rb18
-rw-r--r--ci/phantomjs.js149
-rwxr-xr-xci/travis.rb11
-rw-r--r--guides/CHANGELOG.md3
-rw-r--r--guides/Rakefile46
-rw-r--r--[-rwxr-xr-x]guides/assets/stylesheets/responsive-tables.css0
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb2
-rw-r--r--guides/bug_report_templates/action_controller_master.rb1
-rw-r--r--guides/bug_report_templates/active_job_gem.rb2
-rw-r--r--guides/bug_report_templates/active_job_master.rb1
-rw-r--r--guides/bug_report_templates/active_record_gem.rb2
-rw-r--r--guides/bug_report_templates/active_record_master.rb1
-rw-r--r--guides/bug_report_templates/active_record_migrations_gem.rb2
-rw-r--r--guides/bug_report_templates/active_record_migrations_master.rb1
-rw-r--r--guides/bug_report_templates/generic_gem.rb2
-rw-r--r--guides/bug_report_templates/generic_master.rb1
-rw-r--r--guides/rails_guides.rb36
-rw-r--r--guides/rails_guides/generator.rb167
-rw-r--r--guides/rails_guides/kindle.rb12
-rw-r--r--guides/rails_guides/markdown.rb15
-rw-r--r--guides/rails_guides/markdown/renderer.rb45
-rw-r--r--guides/source/5_0_release_notes.md9
-rw-r--r--guides/source/5_1_release_notes.md277
-rw-r--r--guides/source/_welcome.html.erb7
-rw-r--r--guides/source/action_cable_overview.md35
-rw-r--r--guides/source/action_controller_overview.md2
-rw-r--r--guides/source/action_mailer_basics.md15
-rw-r--r--guides/source/active_job_basics.md2
-rw-r--r--guides/source/active_model_basics.md53
-rw-r--r--guides/source/active_record_callbacks.md20
-rw-r--r--guides/source/active_record_postgresql.md4
-rw-r--r--guides/source/active_record_querying.md7
-rw-r--r--guides/source/active_record_validations.md15
-rw-r--r--guides/source/active_support_core_extensions.md47
-rw-r--r--guides/source/asset_pipeline.md54
-rw-r--r--guides/source/association_basics.md86
-rw-r--r--guides/source/caching_with_rails.md24
-rw-r--r--guides/source/command_line.md14
-rw-r--r--guides/source/configuring.md23
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md18
-rw-r--r--guides/source/debugging_rails_applications.md15
-rw-r--r--guides/source/development_dependencies_install.md6
-rw-r--r--guides/source/documents.yaml5
-rw-r--r--guides/source/engines.md2
-rw-r--r--guides/source/form_helpers.md4
-rw-r--r--guides/source/generators.md30
-rw-r--r--guides/source/getting_started.md21
-rw-r--r--guides/source/i18n.md56
-rw-r--r--guides/source/initialization.md165
-rw-r--r--guides/source/kindle/rails_guides.opf.erb2
-rw-r--r--guides/source/layouts_and_rendering.md4
-rw-r--r--guides/source/maintenance_policy.md6
-rw-r--r--guides/source/routing.md8
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md44
-rw-r--r--guides/source/security.md23
-rw-r--r--guides/source/testing.md208
-rw-r--r--guides/source/upgrading_ruby_on_rails.md35
-rw-r--r--guides/source/working_with_javascript_in_rails.md2
-rw-r--r--guides/w3c_validator.rb9
-rw-r--r--railties/CHANGELOG.md72
-rw-r--r--railties/MIT-LICENSE2
-rw-r--r--railties/RDOC_MAIN.rdoc2
-rw-r--r--railties/Rakefile1
-rwxr-xr-xrailties/bin/test4
-rw-r--r--railties/lib/rails.rb3
-rw-r--r--railties/lib/rails/api/generator.rb28
-rw-r--r--railties/lib/rails/api/task.rb23
-rw-r--r--railties/lib/rails/application.rb22
-rw-r--r--railties/lib/rails/application/bootstrap.rb10
-rw-r--r--railties/lib/rails/application/configuration.rb68
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb4
-rw-r--r--railties/lib/rails/application/finisher.rb1
-rw-r--r--railties/lib/rails/application/routes_reloader.rb17
-rw-r--r--railties/lib/rails/application_controller.rb2
-rw-r--r--railties/lib/rails/command.rb40
-rw-r--r--railties/lib/rails/command/actions.rb16
-rw-r--r--railties/lib/rails/command/base.rb28
-rw-r--r--railties/lib/rails/command/behavior.rb12
-rw-r--r--railties/lib/rails/commands/dbconsole/dbconsole_command.rb6
-rw-r--r--railties/lib/rails/commands/destroy/destroy_command.rb13
-rw-r--r--railties/lib/rails/commands/generate/generate_command.rb11
-rw-r--r--railties/lib/rails/commands/help/USAGE16
-rw-r--r--railties/lib/rails/commands/new/new_command.rb6
-rw-r--r--railties/lib/rails/commands/plugin/plugin_command.rb2
-rw-r--r--railties/lib/rails/commands/rake/rake_command.rb4
-rw-r--r--railties/lib/rails/commands/runner/USAGE2
-rw-r--r--railties/lib/rails/commands/runner/runner_command.rb13
-rw-r--r--railties/lib/rails/commands/secrets/USAGE60
-rw-r--r--railties/lib/rails/commands/secrets/secrets_command.rb49
-rw-r--r--railties/lib/rails/commands/server/server_command.rb164
-rw-r--r--railties/lib/rails/commands/test/test_command.rb8
-rw-r--r--railties/lib/rails/engine.rb39
-rw-r--r--railties/lib/rails/engine/commands.rb17
-rw-r--r--railties/lib/rails/engine/updater.rb19
-rw-r--r--railties/lib/rails/gem_version.rb2
-rw-r--r--railties/lib/rails/generators.rb412
-rw-r--r--railties/lib/rails/generators/actions.rb16
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb10
-rw-r--r--railties/lib/rails/generators/app_base.rb83
-rw-r--r--railties/lib/rails/generators/base.rb30
-rw-r--r--railties/lib/rails/generators/erb.rb6
-rw-r--r--railties/lib/rails/generators/erb/mailer/mailer_generator.rb6
-rw-r--r--railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb16
-rw-r--r--railties/lib/rails/generators/migration.rb2
-rw-r--r--railties/lib/rails/generators/named_base.rb94
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb116
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/update.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/yarn9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/cable.yml1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml25
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt40
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/locales/en.yml10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/secrets.yml10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/package.json5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb5
-rw-r--r--railties/lib/rails/generators/rails/assets/assets_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb25
-rw-r--r--railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb66
-rw-r--r--railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc3
-rw-r--r--railties/lib/rails/generators/rails/generator/generator_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb16
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt1
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/test.tt12
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb5
-rw-r--r--railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb6
-rw-r--r--railties/lib/rails/generators/rails/system_test/USAGE10
-rw-r--r--railties/lib/rails/generators/rails/system_test/system_test_generator.rb7
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/generator/generator_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/system/system_generator.rb17
-rw-r--r--railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb5
-rw-r--r--railties/lib/rails/generators/test_unit/system/templates/system_test.rb9
-rw-r--r--railties/lib/rails/generators/testing/behaviour.rb10
-rw-r--r--railties/lib/rails/info.rb6
-rw-r--r--railties/lib/rails/mailers_controller.rb10
-rw-r--r--railties/lib/rails/paths.rb5
-rw-r--r--railties/lib/rails/plugin/test.rb7
-rw-r--r--railties/lib/rails/rack/debugger.rb3
-rw-r--r--railties/lib/rails/rack/logger.rb66
-rw-r--r--railties/lib/rails/railtie.rb5
-rw-r--r--railties/lib/rails/railtie/configurable.rb2
-rw-r--r--railties/lib/rails/secrets.rb106
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb4
-rw-r--r--railties/lib/rails/tasks.rb1
-rw-r--r--railties/lib/rails/tasks/engine.rake11
-rw-r--r--railties/lib/rails/tasks/framework.rake16
-rw-r--r--railties/lib/rails/tasks/log.rake8
-rw-r--r--railties/lib/rails/tasks/routes.rake9
-rw-r--r--railties/lib/rails/tasks/statistics.rake4
-rw-r--r--railties/lib/rails/tasks/yarn.rake11
-rw-r--r--railties/lib/rails/test_help.rb12
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb57
-rw-r--r--railties/lib/rails/test_unit/railtie.rb1
-rw-r--r--railties/lib/rails/test_unit/test_requirer.rb7
-rw-r--r--railties/lib/rails/test_unit/testing.rake15
-rw-r--r--railties/test/abstract_unit.rb18
-rw-r--r--railties/test/application/assets_test.rb2
-rw-r--r--railties/test/application/bin_setup_test.rb22
-rw-r--r--railties/test/application/configuration/custom_test.rb19
-rw-r--r--railties/test/application/configuration_test.rb83
-rw-r--r--railties/test/application/generators_test.rb25
-rw-r--r--railties/test/application/help_test.rb23
-rw-r--r--railties/test/application/initializers/frameworks_test.rb8
-rw-r--r--railties/test/application/loading_test.rb2
-rw-r--r--railties/test/application/mailer_previews_test.rb34
-rw-r--r--railties/test/application/middleware/cache_test.rb16
-rw-r--r--railties/test/application/middleware/session_test.rb6
-rw-r--r--railties/test/application/middleware_test.rb43
-rw-r--r--railties/test/application/rake/dbs_test.rb5
-rw-r--r--railties/test/application/rake/framework_test.rb1
-rw-r--r--railties/test/application/rake/log_test.rb33
-rw-r--r--railties/test/application/rake/migrations_test.rb16
-rw-r--r--railties/test/application/rake_test.rb80
-rw-r--r--railties/test/application/routing_test.rb187
-rw-r--r--railties/test/application/runner_test.rb8
-rw-r--r--railties/test/application/test_runner_test.rb138
-rw-r--r--railties/test/application/version_test.rb24
-rw-r--r--railties/test/backtrace_cleaner_test.rb2
-rw-r--r--railties/test/code_statistics_calculator_test.rb2
-rw-r--r--railties/test/command/base_test.rb11
-rw-r--r--railties/test/commands/secrets_test.rb37
-rw-r--r--railties/test/commands/server_test.rb107
-rw-r--r--railties/test/engine/commands_tasks_test.rb24
-rw-r--r--railties/test/engine/commands_test.rb96
-rw-r--r--railties/test/generators/actions_test.rb2
-rw-r--r--railties/test/generators/api_app_generator_test.rb30
-rw-r--r--railties/test/generators/app_generator_test.rb166
-rw-r--r--railties/test/generators/controller_generator_test.rb2
-rw-r--r--railties/test/generators/encrypted_secrets_generator_test.rb42
-rw-r--r--railties/test/generators/generator_test.rb8
-rw-r--r--railties/test/generators/integration_test_generator_test.rb8
-rw-r--r--railties/test/generators/migration_generator_test.rb11
-rw-r--r--railties/test/generators/model_generator_test.rb11
-rw-r--r--railties/test/generators/named_base_test.rb2
-rw-r--r--railties/test/generators/plugin_generator_test.rb36
-rw-r--r--railties/test/generators/plugin_test_runner_test.rb11
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb6
-rw-r--r--railties/test/generators/scaffold_generator_test.rb88
-rw-r--r--railties/test/generators/shared_generator_tests.rb1
-rw-r--r--railties/test/generators/system_test_generator_test.rb12
-rw-r--r--railties/test/generators_test.rb2
-rw-r--r--railties/test/isolation/abstract_unit.rb2
-rw-r--r--railties/test/rack_logger_test.rb2
-rw-r--r--railties/test/rails_info_test.rb2
-rw-r--r--railties/test/railties/engine_test.rb2
-rw-r--r--railties/test/railties/mounted_engine_test.rb2
-rw-r--r--railties/test/secrets_test.rb108
-rw-r--r--tasks/release.rb152
-rw-r--r--tools/test.rb5
-rw-r--r--version.rb2
1093 files changed, 25419 insertions, 9397 deletions
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000000..387b8fea9c
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,24 @@
+# Number of days of inactivity before an issue becomes stale
+days: 90
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - pinned
+ - security
+ - With reproduction steps
+ - attached PR
+ - regression
+ - release blocker
+# Label to use when marking an issue as stale
+staleLabel: stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not been commented on for at least three months.
+
+ The resources of the Rails team are limited, and so we are asking for your help.
+
+ If you can still reproduce this error on the `5-1-stable` branch or on `master`,
+ please reply with all of the information you have about it in order to keep the issue open.
+
+ Thank you for all your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.gitignore b/.gitignore
index 9268977c2f..32939b7bfd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
.Gemfile
.ruby-version
+.byebug_history
debug.log
pkg
/.bundle
@@ -19,3 +20,5 @@ pkg
/railties/doc
/railties/tmp
/guides/output
+node_modules/
+/actionview/log
diff --git a/.rubocop.yml b/.rubocop.yml
index e1102b45ce..0d1d0c36ce 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -6,6 +6,7 @@ AllCops:
Exclude:
- '**/templates/**/*'
- '**/vendor/**/*'
+ - 'actionpack/lib/action_dispatch/journey/parser.rb'
# Prefer &&/|| over and/or.
Style/AndOr:
@@ -32,6 +33,10 @@ Style/EmptyLines:
Style/EmptyLinesAroundClassBody:
Enabled: true
+# In a regular method definition, no empty lines around the body.
+Style/EmptyLinesAroundMethodBody:
+ Enabled: true
+
# In a regular module definition, no empty lines around the body.
Style/EmptyLinesAroundModuleBody:
Enabled: true
@@ -112,7 +117,7 @@ Style/UnneededPercentQ:
# assignments, where it should be aligned with the LHS.
Lint/EndAlignment:
Enabled: true
- AlignWith: variable
+ EnforcedStyleAlignWith: variable
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
diff --git a/.travis.yml b/.travis.yml
index af207f4f5d..ed4e0fba3b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,25 +6,33 @@ cache:
directories:
- /tmp/cache/unicode_conformance
- /tmp/beanstalkd-1.10
+ - node_modules
+ - $HOME/.nvm
services:
- memcached
- redis
- - rabbitmq
addons:
postgresql: "9.4"
+ apt:
+ packages:
+ - postgresql-9.4
bundler_args: --without test --jobs 3 --retry 3
before_install:
- "rm ${BUNDLE_GEMFILE}.lock"
+ - "gem update --system"
- "gem update 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"
+ - "[[ $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:
- - bundle update
+before_script:
# Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted.
# Decodes to e.g. `export VARIABLE=VALUE`
- $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4")
@@ -37,41 +45,75 @@ env:
- "JRUBY_OPTS='--dev -J-Xmx1024M'"
matrix:
- "GEM=railties"
- - "GEM=ap"
- - "GEM=ac"
- - "GEM=ac:integration"
+ - "GEM=ap,ac"
- "GEM=am,amo,as,av,aj"
- "GEM=as PRESERVE_TIMEZONES=1"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
- "GEM=ar:postgresql"
- - "GEM=aj:integration"
- "GEM=guides"
+ - "GEM=ac:integration"
rvm:
- 2.2.6
- - 2.3.2
+ - 2.3.3
+ - 2.4.1
- ruby-head
matrix:
include:
- # Latest compiled version in http://rubies.travis-ci.org
- - rvm: 2.3.2
+ - rvm: 2.4.1
+ env: "GEM=av:ujs"
+ - rvm: 2.2.6
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis
+ - rabbitmq
+ - rvm: 2.3.3
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis
+ - rabbitmq
+ - rvm: 2.4.1
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis
+ - rabbitmq
+ - rvm: ruby-head
+ env: "GEM=aj:integration"
+ services:
+ - memcached
+ - redis
+ - rabbitmq
+ - rvm: 2.3.3
env:
- - "GEM=ar:mysql2"
+ - "GEM=ar:mysql2 MYSQL=mariadb"
addons:
mariadb: 10.0
- - rvm: jruby-9.1.5.0
+ - rvm: 2.4.1
+ env:
+ - "GEM=ar:sqlite3_mem"
+ - rvm: jruby-9.1.8.0
jdk: oraclejdk8
env:
- "GEM=ap"
- - rvm: jruby-9.1.5.0
+ - rvm: jruby-9.1.8.0
jdk: oraclejdk8
env:
- - "GEM=am,aj"
+ - "GEM=am,amo,aj"
+ # Test with old (< 9.4.2) postgresql
+ - rvm: 2.4.0
+ env:
+ - "GEM=ar:postgresql"
+ addons:
+ postgresql: "9.4"
allow_failures:
- rvm: ruby-head
- - rvm: jruby-9.1.5.0
+ - rvm: jruby-9.1.8.0
+ - env: "GEM=ac:integration"
fast_finish: true
notifications:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f6ebef7e89..b44486c75a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,9 @@
#### **Did you find a bug?**
+* **Do not open up a GitHub issue if the bug is a security vulnerability
+ in Rails**, and instead to refer to our [security policy](http://rubyonrails.org/security/).
+
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/rails/rails/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
diff --git a/Gemfile b/Gemfile
index 37e5b4fd25..5077c257d9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,6 +7,8 @@ end
gemspec
+gem "arel", github: "rails/arel"
+
# We need a newish Rake since Active Job sets its test tasks' descriptions.
gem "rake", ">= 11.1"
@@ -14,6 +16,8 @@ gem "rake", ">= 11.1"
# be loaded after loading the test library.
gem "mocha", "~> 0.14", require: false
+gem "capybara", "~> 2.13.0"
+
gem "rack-cache", "~> 1.2"
gem "jquery-rails"
gem "coffee-rails"
@@ -29,20 +33,19 @@ gem "bcrypt", "~> 3.1.11", require: false
# sprockets.
gem "uglifier", ">= 1.3.0", require: false
-# Track stable branch of sass because it doesn't have circular require warnings.
-gem "sass", github: "sass/sass", branch: "stable", require: false
-
# FIXME: Remove this fork after https://github.com/nex3/rb-inotify/pull/49 is fixed.
gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false
# Explicitly avoid 1.x that doesn't support Ruby 2.4+
gem "json", ">= 2.0.0"
+gem "rubocop", ">= 0.47", require: false
+
group :doc do
- gem "sdoc", "1.0.0.beta2"
+ gem "sdoc", "1.0.0.rc1"
gem "redcarpet", "~> 3.2.3", platforms: :ruby
gem "w3c_validators"
- gem "kindlerb", ">= 1.0.1"
+ gem "kindlerb", "~> 1.2.0"
end
# Active Support.
@@ -50,13 +53,16 @@ gem "dalli", ">= 2.2.1"
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
+
# Active Job.
group :job do
- gem "resque", github: "resque/resque", require: false
+ gem "resque", require: false
gem "resque-scheduler", require: false
gem "sidekiq", require: false
gem "sucker_punch", require: false
- gem "delayed_job", require: false, github: "collectiveidea/delayed_job"
+ gem "delayed_job", require: false
gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby
gem "sneakers", require: false
gem "que", require: false
@@ -64,7 +70,7 @@ group :job do
#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, github: "collectiveidea/delayed_job_active_record"
+ gem "delayed_job_active_record", require: false
gem "sequel", require: false
end
@@ -80,6 +86,7 @@ group :cable do
gem "blade", require: false, platforms: [:ruby]
gem "blade-sauce_labs_plugin", require: false, platforms: [:ruby]
+ gem "sprockets-export", require: false
end
# Add your own local bundler stuff.
diff --git a/Gemfile.lock b/Gemfile.lock
index bec01cda4a..0c9337bf2a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -7,21 +7,6 @@ GIT
pg (>= 0.17, < 0.20)
GIT
- remote: https://github.com/collectiveidea/delayed_job.git
- revision: e3772d4f0c8470d0fcba00c86ca3bc4f5e876830
- specs:
- delayed_job (4.1.2)
- activesupport (>= 3.0, < 5.1)
-
-GIT
- remote: https://github.com/collectiveidea/delayed_job_active_record.git
- revision: 36f434c4fd660e8f11ce932be117e9c71dde7212
- specs:
- delayed_job_active_record (4.1.1)
- activerecord (>= 3.0, < 5.1)
- delayed_job (>= 3.0, < 5)
-
-GIT
remote: https://github.com/matthewd/rb-inotify.git
revision: 90553518d1fb79aedc98a3036c59bd2b6731ac40
branch: close-handling
@@ -39,78 +24,66 @@ GIT
websocket
GIT
- remote: https://github.com/resque/resque.git
- revision: 20d885065ac19e7f7d7a982f4ed1296083db0300
- specs:
- resque (1.27.0)
- mono_logger (~> 1.0)
- multi_json (~> 1.0)
- redis-namespace (~> 1.3)
- sinatra (>= 0.9.2)
- vegas (~> 0.1.2)
-
-GIT
- remote: https://github.com/sass/sass.git
- revision: 7716e67f3507c6f65878c69aa49ec358ebf675c7
- branch: stable
+ remote: https://github.com/rails/arel.git
+ revision: 437aa3a4bb8ad4f3f4eba299dbb1112852f9c7ac
specs:
- sass (3.4.22)
+ arel (8.0.0)
PATH
remote: .
specs:
- actioncable (5.1.0.alpha)
- actionpack (= 5.1.0.alpha)
- nio4r (~> 1.2)
+ actioncable (5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
- actionmailer (5.1.0.alpha)
- actionpack (= 5.1.0.alpha)
- actionview (= 5.1.0.alpha)
- activejob (= 5.1.0.alpha)
+ actionmailer (5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ actionview (= 5.2.0.alpha)
+ activejob (= 5.2.0.alpha)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.1.0.alpha)
- actionview (= 5.1.0.alpha)
- activesupport (= 5.1.0.alpha)
+ actionpack (5.2.0.alpha)
+ actionview (= 5.2.0.alpha)
+ activesupport (= 5.2.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.1.0.alpha)
- activesupport (= 5.1.0.alpha)
+ actionview (5.2.0.alpha)
+ activesupport (= 5.2.0.alpha)
builder (~> 3.1)
- erubis (~> 2.7.0)
+ erubi (~> 1.4)
rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (5.1.0.alpha)
- activesupport (= 5.1.0.alpha)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (5.2.0.alpha)
+ activesupport (= 5.2.0.alpha)
globalid (>= 0.3.6)
- activemodel (5.1.0.alpha)
- activesupport (= 5.1.0.alpha)
- activerecord (5.1.0.alpha)
- activemodel (= 5.1.0.alpha)
- activesupport (= 5.1.0.alpha)
- arel (~> 7.0)
- activesupport (5.1.0.alpha)
+ 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 (~> 8.0)
+ activesupport (5.2.0.alpha)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
- rails (5.1.0.alpha)
- actioncable (= 5.1.0.alpha)
- actionmailer (= 5.1.0.alpha)
- actionpack (= 5.1.0.alpha)
- actionview (= 5.1.0.alpha)
- activejob (= 5.1.0.alpha)
- activemodel (= 5.1.0.alpha)
- activerecord (= 5.1.0.alpha)
- activesupport (= 5.1.0.alpha)
+ 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)
+ activesupport (= 5.2.0.alpha)
bundler (>= 1.3.0, < 2.0)
- railties (= 5.1.0.alpha)
+ railties (= 5.2.0.alpha)
sprockets-rails (>= 2.0.0)
- railties (5.1.0.alpha)
- actionpack (= 5.1.0.alpha)
- activesupport (= 5.1.0.alpha)
+ railties (5.2.0.alpha)
+ actionpack (= 5.2.0.alpha)
+ activesupport (= 5.2.0.alpha)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -118,9 +91,10 @@ PATH
GEM
remote: https://rubygems.org/
specs:
- addressable (2.4.0)
- amq-protocol (2.0.1)
- arel (7.1.2)
+ addressable (2.5.0)
+ public_suffix (~> 2.0, >= 2.0.2)
+ amq-protocol (2.1.0)
+ ast (2.3.0)
backburner (1.3.1)
beaneater (~> 1.0)
concurrent-ruby (~> 1.0.1)
@@ -130,28 +104,34 @@ GEM
bcrypt (3.1.11-x86-mingw32)
beaneater (1.0.0)
benchmark-ips (2.7.2)
- blade (0.5.6)
+ blade (0.7.0)
activesupport (>= 3.0.0)
- blade-qunit_adapter (~> 1.20.0)
+ blade-qunit_adapter (~> 2.0.1)
coffee-script
coffee-script-source
curses (~> 1.0.0)
eventmachine
faye
sprockets (>= 3.0)
- sprockets-export (~> 0.9.1)
thin (>= 1.6.0)
thor (~> 0.19.1)
useragent (~> 0.16.7)
- blade-qunit_adapter (1.20.0)
- blade-sauce_labs_plugin (0.5.3)
+ blade-qunit_adapter (2.0.1)
+ blade-sauce_labs_plugin (0.6.2)
childprocess
faraday
selenium-webdriver
- builder (3.2.2)
- bunny (2.2.2)
+ builder (3.2.3)
+ bunny (2.6.2)
amq-protocol (>= 2.0.1)
byebug (9.0.6)
+ capybara (2.13.0)
+ addressable
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11)
coffee-rails (4.2.1)
@@ -160,14 +140,19 @@ GEM
coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.10.0)
- concurrent-ruby (1.0.2)
- connection_pool (2.2.0)
+ coffee-script-source (1.12.2)
+ concurrent-ruby (1.0.4)
+ connection_pool (2.2.1)
cookiejar (0.3.3)
curses (1.0.2)
daemons (1.2.4)
dalli (2.7.6)
dante (0.2.0)
+ delayed_job (4.1.2)
+ activesupport (>= 3.0, < 5.1)
+ delayed_job_active_record (4.1.1)
+ activerecord (>= 3.0, < 5.1)
+ delayed_job (>= 3.0, < 5)
em-hiredis (0.3.1)
eventmachine (~> 1.0)
hiredis (~> 0.6.0)
@@ -179,15 +164,16 @@ GEM
http_parser.rb (>= 0.6.0)
em-socksify (0.3.1)
eventmachine (>= 1.0.0.beta.4)
+ erubi (1.6.0)
erubis (2.7.0)
event_emitter (0.2.5)
- eventmachine (1.2.0.1)
- eventmachine (1.2.0.1-x64-mingw32)
- eventmachine (1.2.0.1-x86-mingw32)
+ eventmachine (1.2.1)
+ eventmachine (1.2.1-x64-mingw32)
+ eventmachine (1.2.1-x86-mingw32)
execjs (2.7.0)
- faraday (0.9.2)
+ faraday (0.11.0)
multipart-post (>= 1.2, < 3)
- faye (1.2.2)
+ faye (1.2.3)
cookiejar (>= 0.3.0)
em-http-request (>= 0.3.0)
eventmachine (>= 0.12.0)
@@ -195,23 +181,23 @@ GEM
multi_json (>= 1.0.0)
rack (>= 1.0.0)
websocket-driver (>= 0.5.1)
- faye-websocket (0.10.4)
+ faye-websocket (0.10.5)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
- ffi (1.9.14)
- ffi (1.9.14-x64-mingw32)
- ffi (1.9.14-x86-mingw32)
+ ffi (1.9.17)
+ ffi (1.9.17-x64-mingw32)
+ ffi (1.9.17-x86-mingw32)
globalid (0.3.7)
activesupport (>= 4.1.0)
hiredis (0.6.1)
http_parser.rb (0.6.0)
- i18n (0.7.0)
- jquery-rails (4.2.1)
+ i18n (0.8.1)
+ jquery-rails (4.2.2)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
- json (2.0.2)
- kindlerb (1.0.1)
+ json (2.0.3)
+ kindlerb (1.2.0)
mustache
nokogiri
libxml-ruby (2.9.0)
@@ -239,18 +225,22 @@ GEM
mysql2 (0.4.5)
mysql2 (0.4.5-x64-mingw32)
mysql2 (0.4.5-x86-mingw32)
- nio4r (1.2.1)
- nokogiri (1.6.8.1)
+ nio4r (2.0.0)
+ nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0)
- nokogiri (1.6.8.1-x64-mingw32)
+ nokogiri (1.7.0.1-x64-mingw32)
mini_portile2 (~> 2.1.0)
- nokogiri (1.6.8.1-x86-mingw32)
+ nokogiri (1.7.0.1-x86-mingw32)
mini_portile2 (~> 2.1.0)
+ parser (2.4.0.0)
+ ast (~> 2.2)
pg (0.19.0)
pg (0.19.0-x64-mingw32)
pg (0.19.0-x86-mingw32)
- psych (2.1.1)
- puma (3.6.0)
+ powerpack (0.1.1)
+ psych (2.2.2)
+ public_suffix (2.0.5)
+ puma (3.7.0)
qu (0.2.0)
multi_json
qu-redis (0.2.0)
@@ -266,56 +256,72 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails-dom-testing (2.0.1)
+ rails-dom-testing (2.0.2)
activesupport (>= 4.2.0, < 6.0)
- nokogiri (~> 1.6.0)
+ nokogiri (~> 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- rake (11.3.0)
- rb-fsevent (0.9.7)
- rdoc (5.0.0.beta2)
+ rainbow (2.2.1)
+ rake (12.0.0)
+ rb-fsevent (0.9.8)
+ rdoc (5.0.0)
redcarpet (3.2.3)
- redis (3.3.1)
+ redis (3.3.2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
+ resque (1.27.0)
+ mono_logger (~> 1.0)
+ multi_json (~> 1.0)
+ redis-namespace (~> 1.3)
+ sinatra (>= 0.9.2)
+ vegas (~> 0.1.2)
resque-scheduler (4.3.0)
mono_logger (~> 1.0)
redis (~> 3.3)
resque (~> 1.26)
rufus-scheduler (~> 3.2)
- ruby_dep (1.4.0)
+ rubocop (0.47.1)
+ parser (>= 2.3.3.1, < 3.0)
+ powerpack (~> 0.1)
+ rainbow (>= 1.99.1, < 3.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (~> 1.0, >= 1.0.1)
+ ruby-progressbar (1.8.1)
+ ruby_dep (1.5.0)
rubyzip (1.2.0)
- rufus-scheduler (3.2.2)
+ rufus-scheduler (3.3.2)
+ tzinfo
+ sass (3.4.23)
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)
- sdoc (1.0.0.beta2)
- rdoc (= 5.0.0.beta2)
- selenium-webdriver (2.53.4)
+ sdoc (1.0.0.rc1)
+ rdoc (= 5.0.0)
+ selenium-webdriver (3.0.5)
childprocess (~> 0.5)
rubyzip (~> 1.0)
websocket (~> 1.0)
- sequel (4.39.0)
+ sequel (4.42.1)
serverengine (1.5.11)
sigdump (~> 0.2.2)
- sidekiq (4.2.2)
+ sidekiq (4.2.9)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
- rack-protection (~> 1.5)
+ rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
sigdump (0.2.4)
simple_uuid (0.4.0)
sinatra (1.0)
rack (>= 1.0)
- sneakers (2.3.5)
- bunny (~> 2.2.0)
+ sneakers (2.4.0)
+ bunny (~> 2.6)
serverengine (~> 1.5.11)
thor
thread (~> 0.1.7)
- sprockets (3.7.0)
+ sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-export (0.9.1)
@@ -323,9 +329,9 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sqlite3 (1.3.12)
- sqlite3 (1.3.12-x64-mingw32)
- sqlite3 (1.3.12-x86-mingw32)
+ sqlite3 (1.3.13)
+ sqlite3 (1.3.13-x64-mingw32)
+ sqlite3 (1.3.13-x86-mingw32)
stackprof (0.2.10)
sucker_punch (2.0.2)
concurrent-ruby (~> 1.0.0)
@@ -333,7 +339,7 @@ GEM
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
- thor (0.19.1)
+ thor (0.19.4)
thread (0.1.7)
thread_safe (0.3.5)
tilt (2.0.5)
@@ -342,21 +348,24 @@ GEM
turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- tzinfo-data (1.2016.7)
+ tzinfo-data (1.2016.10)
tzinfo (>= 1.0.0)
- uglifier (3.0.2)
+ uglifier (3.0.4)
execjs (>= 0.3.0, < 3)
+ unicode-display_width (1.1.3)
useragent (0.16.8)
vegas (0.1.11)
rack (>= 1.0.0)
- w3c_validators (1.2)
- json
- nokogiri
+ w3c_validators (1.3.1)
+ json (~> 2.0)
+ nokogiri (~> 1.6)
wdm (0.1.1)
websocket (1.2.3)
websocket-driver (0.6.4)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
PLATFORMS
ruby
@@ -367,21 +376,24 @@ DEPENDENCIES
activerecord-jdbcmysql-adapter (>= 1.3.0)
activerecord-jdbcpostgresql-adapter (>= 1.3.0)
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
+ arel!
backburner
bcrypt (~> 3.1.11)
benchmark-ips
blade
blade-sauce_labs_plugin
byebug
+ capybara (~> 2.13.0)
coffee-rails
dalli (>= 2.2.1)
- delayed_job!
- delayed_job_active_record!
+ delayed_job
+ delayed_job_active_record
em-hiredis
+ erubis (~> 2.7.0)
hiredis
jquery-rails
json (>= 2.0.0)
- kindlerb (>= 1.0.1)
+ kindlerb (~> 1.2.0)
libxml-ruby
listen (>= 3.0.5, < 3.2)
minitest (< 5.3.4)
@@ -401,14 +413,15 @@ DEPENDENCIES
rb-inotify!
redcarpet (~> 3.2.3)
redis
- resque!
+ resque
resque-scheduler
- sass!
+ rubocop (>= 0.47)
sass-rails
- sdoc (= 1.0.0.beta2)
+ sdoc (= 1.0.0.rc1)
sequel
sidekiq
sneakers
+ sprockets-export
sqlite3 (~> 1.3.6)
stackprof
sucker_punch
@@ -420,4 +433,4 @@ DEPENDENCIES
websocket-client-simple!
BUNDLED WITH
- 1.13.6
+ 1.14.5
diff --git a/MIT-LICENSE b/MIT-LICENSE
index 40235833ba..6b3cead1a7 100644
--- a/MIT-LICENSE
+++ b/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2016 David Heinemeier Hansson
+Copyright (c) 2005-2017 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 8ea1016081..d5ebf861d3 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-5.1.0.alpha
+5.2.0.alpha
diff --git a/README.md b/README.md
index a2b726ea6c..7e25a0c8f1 100644
--- a/README.md
+++ b/README.md
@@ -71,13 +71,17 @@ and may also be used independently outside Rails.
* [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html)
* [Ruby on Rails Guides](http://guides.rubyonrails.org)
* [The API Documentation](http://api.rubyonrails.org)
- * [Ruby on Rails Tutorial](http://www.railstutorial.org/book)
+ * [Ruby on Rails Tutorial](https://www.railstutorial.org/book)
## Contributing
We encourage you to contribute to Ruby on Rails! Please check out the
[Contributing to Ruby on Rails guide](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](http://contributors.rubyonrails.org)
+Trying to report a possible security vulnerability in Rails? Please
+check out our [security policy](http://rubyonrails.org/security/) for
+guidelines about how to proceed.
+
Everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/).
## Code Status
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md
index 4e6b811d3e..6e27d8ee5a 100644
--- a/RELEASING_RAILS.md
+++ b/RELEASING_RAILS.md
@@ -58,7 +58,7 @@ lists:
Implementors will love you and help you.
-### 3 Days before release
+## 3 Days before release
This is when you should release the release candidate. Here are your tasks
for today:
@@ -103,17 +103,19 @@ branch.
Run `rake install` to generate the gems and install them locally. Then try
generating a new app and ensure that nothing explodes.
-Verify that Action Cable's package.json is updated with the RC version.
+Verify that Action Cable and Action View's package.json files are updated with
+the RC version.
This will stop you from looking silly when you push an RC to rubygems.org and
then realize it is broken.
### Release to RubyGems and NPM.
-IMPORTANT: The Action Cable client is released as an NPM package, so you must
-have Node.js installed, have an NPM account (npmjs.com), and be an actioncable
-package owner (`npm owner ls actioncable`) to do a full release. Do not release
-until you're set up with NPM!
+IMPORTANT: The Action Cable client and Action View's UJS adapter are released
+as NPM packages, so you must have Node.js installed, have an NPM account
+(npmjs.com), and be a package owner for `actioncable` and `rails-ujs` (you can
+check this via `npm owner ls actioncable` and `npm owner ls rails-ujs`) in
+order to do a full release. Do not release until you're set up with NPM!
Run `rake release`. This will populate the gemspecs and NPM package.json with
the current RAILS_VERSION, commit the changes, tag it, and push the gems to
@@ -179,7 +181,7 @@ more explanation on a particular step, see the RC steps.
Today, do this stuff in this order:
* Apply security patches to the release branch
-* Update CHANGELOG with security fixes.
+* Update CHANGELOG with security fixes
* Update RAILS_VERSION to remove the rc
* Build and test the gem
* Release the gems
@@ -206,7 +208,7 @@ so we need to give them the security fixes in patch form.
* Blog announcements
* Twitter announcements
-* Merge the release branch to the stable branch.
+* Merge the release branch to the stable branch
* Drink beer (or other cocktail)
## Misc
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 2c84d3158f..d5bd58cfdb 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,37 +1 @@
-* Permit same-origin connections by default.
-
- Added new option `config.action_cable.allow_same_origin_as_host = false`
- to disable this behaviour.
-
- *Dávid Halász*, *Matthew Draper*
-
-* Prevent race where the client could receive and act upon a
- subscription confirmation before the channel's `subscribed` method
- completed.
-
- Fixes #25381.
-
- *Vladimir Dementyev*
-
-* Buffer now writes to websocket connections, to avoid blocking threads
- that could be doing more useful things.
-
- *Matthew Draper*, *Tinco Andringa*
-
-* Protect against concurrent writes to a websocket connection from
- multiple threads; the underlying OS write is not always threadsafe.
-
- *Tinco Andringa*
-
-* Add `ActiveSupport::Notifications` hook to `Broadcaster#broadcast`.
-
- *Matthew Wear*
-
-* Close hijacked socket when connection is shut down.
-
- Fixes #25613.
-
- *Tinco Andringa*
-
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md) for previous changes.
diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE
index 27a17cf41b..1a0e653b69 100644
--- a/actioncable/MIT-LICENSE
+++ b/actioncable/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015-2016 Basecamp, LLC
+Copyright (c) 2015-2017 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 8ad9aeb1f1..e044f54b45 100644
--- a/actioncable/README.md
+++ b/actioncable/README.md
@@ -51,10 +51,10 @@ module ApplicationCable
self.current_user = find_verified_user
end
- protected
+ private
def find_verified_user
- if current_user = User.find_by(id: cookies.signed[:user_id])
- current_user
+ if verified_user = User.find_by(id: cookies.signed[:user_id])
+ verified_user
else
reject_unauthorized_connection
end
@@ -536,6 +536,15 @@ cable.subscriptions.create 'AppearanceChannel',
# normal channel code goes here...
```
+## Download and Installation
+
+The latest version of Action Cable can be installed with [RubyGems](#gem-usage),
+or with [npm](#npm-usage).
+
+Source code can be downloaded as part of the Rails project on GitHub
+
+* https://github.com/rails/rails/tree/master/actioncable
+
## License
Action Cable is released under the MIT license:
diff --git a/actioncable/Rakefile b/actioncable/Rakefile
index 87d443919c..bda8c7b6c8 100644
--- a/actioncable/Rakefile
+++ b/actioncable/Rakefile
@@ -1,12 +1,13 @@
require "rake/testtask"
require "pathname"
+require "open3"
require "action_cable"
dir = File.dirname(__FILE__)
task default: :test
-task package: "assets:compile"
+task package: %w( assets:compile assets:verify )
Rake::TestTask.new do |t|
t.libs << "test"
@@ -37,6 +38,39 @@ namespace :assets do
desc "Compile Action Cable assets"
task :compile do
require "blade"
+ require "sprockets"
+ require "sprockets/export"
Blade.build
end
+
+ desc "Verify compiled Action Cable assets"
+ task :verify do
+ file = "lib/assets/compiled/action_cable.js"
+ pathname = Pathname.new("#{dir}/#{file}")
+
+ print "[verify] #{file} exists "
+ if pathname.exist?
+ puts "[OK]"
+ else
+ $stderr.puts "[FAIL]"
+ fail
+ end
+
+ print "[verify] #{file} is a UMD module "
+ if pathname.read =~ /module\.exports.*define\.amd/m
+ puts "[OK]"
+ else
+ $stderr.puts "[FAIL]"
+ fail
+ end
+
+ print "[verify] #{dir} can be required as a module "
+ stdout, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{dir}');")
+ if status.success?
+ puts "[OK]"
+ else
+ $stderr.puts "[FAIL]\n#{stderr}"
+ fail
+ end
+ end
end
diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec
index e7f91d0e34..6d95f022fa 100644
--- a/actioncable/actioncable.gemspec
+++ b/actioncable/actioncable.gemspec
@@ -20,6 +20,6 @@ Gem::Specification.new do |s|
s.add_dependency "actionpack", version
- s.add_dependency "nio4r", "~> 1.2"
+ s.add_dependency "nio4r", "~> 2.0"
s.add_dependency "websocket-driver", "~> 0.6.1"
end
diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee
index 29ad676290..7fd68cad2f 100644
--- a/actioncable/app/assets/javascripts/action_cable/connection.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee
@@ -23,7 +23,7 @@ class ActionCable.Connection
open: =>
if @isActive()
ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}")
- throw new Error("Existing connection must be closed before opening")
+ false
else
ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}")
@uninstallEventHandlers() if @webSocket?
diff --git a/actioncable/bin/test b/actioncable/bin/test
new file mode 100755
index 0000000000..a7beb14b27
--- /dev/null
+++ b/actioncable/bin/test
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require File.expand_path("../tools/test", COMPONENT_ROOT)
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
index d353716636..c2d3550acb 100644
--- a/actioncable/lib/action_cable.rb
+++ b/actioncable/lib/action_cable.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2015-2016 Basecamp, LLC
+# Copyright (c) 2015-2017 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/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index a866044f95..718f630f58 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -122,16 +122,16 @@ module ActionCable
end
end
- protected
+ private
# action_methods are cached and there is sometimes need to refresh
# them. ::clear_action_methods! allows you to do that, so next time
# you run action_methods, they will be recalculated.
- def clear_action_methods!
+ def clear_action_methods! # :doc:
@action_methods = nil
end
# Refresh the cached action_methods when a new action_method is added.
- def method_added(name)
+ def method_added(name) # :doc:
super
clear_action_methods!
end
@@ -189,23 +189,23 @@ module ActionCable
end
end
- protected
+ private
# Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams
# you want this channel to be sending to the subscriber.
- def subscribed
+ def subscribed # :doc:
# Override in subclasses
end
# Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
# users as offline or the like.
- def unsubscribed
+ def unsubscribed # :doc:
# Override in subclasses
end
# Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
# the proper channel identifier marked as the recipient.
- def transmit(data, via: nil)
- logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via }
+ def transmit(data, via: nil) # :doc:
+ logger.debug "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via }
payload = { channel_class: self.class.name, data: data, via: via }
ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
@@ -213,33 +213,32 @@ module ActionCable
end
end
- def ensure_confirmation_sent
+ def ensure_confirmation_sent # :doc:
return if subscription_rejected?
@defer_subscription_confirmation_counter.decrement
transmit_subscription_confirmation unless defer_subscription_confirmation?
end
- def defer_subscription_confirmation!
+ def defer_subscription_confirmation! # :doc:
@defer_subscription_confirmation_counter.increment
end
- def defer_subscription_confirmation?
+ def defer_subscription_confirmation? # :doc:
@defer_subscription_confirmation_counter.value > 0
end
- def subscription_confirmation_sent?
+ def subscription_confirmation_sent? # :doc:
@subscription_confirmation_sent
end
- def reject
+ def reject # :doc:
@reject_subscription = true
end
- def subscription_rejected?
+ def subscription_rejected? # :doc:
@reject_subscription
end
- private
def delegate_connection_identifiers
connection.identifiers.each do |identifier|
define_singleton_method(identifier) do
diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb
index 85df206445..989a67d6df 100644
--- a/actioncable/lib/action_cable/connection/authorization.rb
+++ b/actioncable/lib/action_cable/connection/authorization.rb
@@ -3,11 +3,11 @@ module ActionCable
module Authorization
class UnauthorizedError < StandardError; end
- private
- def reject_unauthorized_connection
- logger.error "An unauthorized connection attempt was rejected"
- raise UnauthorizedError
- end
+ # Closes the \WebSocket connection if it is open and returns a 404 "File not Found" response.
+ def reject_unauthorized_connection
+ logger.error "An unauthorized connection attempt was rejected"
+ raise UnauthorizedError
+ end
end
end
end
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index dfee123ea2..0a517a532d 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -22,13 +22,10 @@ module ActionCable
# # Any cleanup work needed when the cable connection is cut.
# end
#
- # protected
+ # private
# def find_verified_user
- # if current_user = User.find_by_identity cookies.signed[:identity_id]
- # current_user
- # else
+ # User.find_by_identity(cookies.signed[:identity_id]) ||
# reject_unauthorized_connection
- # end
# end
# end
# end
@@ -136,9 +133,15 @@ 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
+ 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
+ def request # :doc:
@request ||= begin
environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
ActionDispatch::Request.new(environment || env)
@@ -146,14 +149,10 @@ module ActionCable
end
# The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
- def cookies
+ def cookies # :doc:
request.cookie_jar
end
- attr_reader :websocket
- attr_reader :message_buffer
-
- private
def encode(cable_message)
@coder.encode cable_message
end
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
index 70a2bbecb1..c7e30e78c8 100644
--- a/actioncable/lib/action_cable/connection/client_socket.rb
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -90,8 +90,8 @@ module ActionCable
reason ||= ""
unless code == 1000 || (code >= 3000 && code <= 4999)
- raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
- "The code must be either 1000, or between 3000 and 4999. " +
+ raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
+ "The code must be either 1000, or between 3000 and 4999. " \
"#{code} is neither."
end
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
index 6a80770cae..4ccd322644 100644
--- a/actioncable/lib/action_cable/connection/message_buffer.rb
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -28,6 +28,8 @@ 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
attr_reader :connection
attr_reader :buffered_messages
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index 00511aead5..44bce1e195 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -19,7 +19,7 @@ module ActionCable
logger.error "Received unrecognized command in #{data.inspect}"
end
rescue Exception => e
- logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
+ logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
end
def add(data)
@@ -61,6 +61,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
diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
index 41afa9680a..aef549aa86 100644
--- a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
+++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
@@ -31,8 +31,8 @@ module ActionCable
end
end
- protected
- def log(type, message)
+ private
+ def log(type, message) # :doc:
tag(@logger) { @logger.send type, message }
end
end
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 382141b89f..03eb6e2ea8 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -32,6 +32,8 @@ 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
attr_reader :websocket
end
diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb
index e23527b84e..63a26636a0 100644
--- a/actioncable/lib/action_cable/engine.rb
+++ b/actioncable/lib/action_cable/engine.rb
@@ -31,7 +31,7 @@ module ActionCable
self.cable = Rails.application.config_for(config_path).with_indifferent_access
end
- previous_connection_class = self.connection_class
+ previous_connection_class = connection_class
self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
options.each { |k, v| send("#{k}=", v) }
diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb
index 8ba0230d47..5d6f9af0bb 100644
--- a/actioncable/lib/action_cable/gem_version.rb
+++ b/actioncable/lib/action_cable/gem_version.rb
@@ -6,7 +6,7 @@ module ActionCable
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb
index 1fc58baa3e..7fcd6c6587 100644
--- a/actioncable/lib/action_cable/server/broadcasting.rb
+++ b/actioncable/lib/action_cable/server/broadcasting.rb
@@ -38,7 +38,7 @@ module ActionCable
end
def broadcast(message)
- server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
+ server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
payload = { broadcasting: broadcasting, message: message, coder: coder }
ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb
index 72e62f3daf..596269ab9b 100644
--- a/actioncable/lib/action_cable/subscription_adapter.rb
+++ b/actioncable/lib/action_cable/subscription_adapter.rb
@@ -4,5 +4,6 @@ module ActionCable
autoload :Base
autoload :SubscriberMap
+ autoload :ChannelPrefix
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb
new file mode 100644
index 0000000000..8b293cc785
--- /dev/null
+++ b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb
@@ -0,0 +1,26 @@
+module ActionCable
+ module SubscriptionAdapter
+ module ChannelPrefix # :nodoc:
+ def broadcast(channel, payload)
+ channel = channel_with_prefix(channel)
+ super
+ end
+
+ def subscribe(channel, callback, success_callback = nil)
+ channel = channel_with_prefix(channel)
+ super
+ end
+
+ def unsubscribe(channel, callback)
+ channel = channel_with_prefix(channel)
+ super
+ end
+
+ private
+ # Returns the channel name, including channel_prefix specified in cable.yml
+ def channel_with_prefix(channel)
+ [@server.config.cable[:channel_prefix], channel].compact.join(":")
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
index bcd46d2a0e..ed8f315791 100644
--- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
@@ -11,6 +11,8 @@ 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.
@@ -22,6 +24,12 @@ module ActionCable
cattr_accessor(:redis_connector) { ->(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
@@ -68,10 +76,10 @@ module ActionCable
end
def ensure_reactor_running
- return if EventMachine.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?
+ Thread.pass until EventMachine.reactor_running? && EventMachine.reactor_thread
end
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
index 62bd284a6b..41a6e55822 100644
--- a/actioncable/lib/action_cable/subscription_adapter/redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -6,6 +6,8 @@ require "redis"
module ActionCable
module SubscriptionAdapter
class Redis < Base # :nodoc:
+ prepend ChannelPrefix
+
# Overwrite this factory method for redis connections if you want to use a different Redis library than Redis.
# This is needed, for example, when using Makara proxies for distributed Redis.
cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } }
diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE
index 6249553c22..dd109fda80 100644
--- a/actioncable/lib/rails/generators/channel/USAGE
+++ b/actioncable/lib/rails/generators/channel/USAGE
@@ -3,7 +3,7 @@ Description:
Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript).
Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments.
- Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels.
+ Note: Turn on the cable connection in app/assets/javascripts/cable.js after generating any channels.
Example:
========
@@ -11,4 +11,4 @@ Example:
creates a Chat channel class and CoffeeScript asset:
Channel: app/channels/chat_channel.rb
- Assets: app/assets/javascript/channels/chat.coffee
+ Assets: app/assets/javascripts/channels/chat.coffee
diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb
index 20d807c033..984b78bc9c 100644
--- a/actioncable/lib/rails/generators/channel/channel_generator.rb
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -13,7 +13,7 @@ module Rails
template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
if options[:assets]
- if self.behavior == :invoke
+ if behavior == :invoke
template "assets/cable.js", "app/assets/javascripts/cable.js"
end
@@ -23,14 +23,14 @@ module Rails
generate_application_cable_files
end
- protected
+ private
def file_name
@_file_name ||= super.gsub(/_channel/i, "")
end
# FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
def generate_application_cable_files
- return if self.behavior != :invoke
+ return if behavior != :invoke
files = [
"application_cable/channel.rb",
diff --git a/actioncable/package.json b/actioncable/package.json
index 37f82fa1ea..acec1e2e9c 100644
--- a/actioncable/package.json
+++ b/actioncable/package.json
@@ -1,6 +1,6 @@
{
"name": "actioncable",
- "version": "5.0.0-rc1",
+ "version": "5.2.0-alpha",
"description": "WebSocket framework for Ruby on Rails.",
"main": "lib/assets/compiled/action_cable.js",
"files": [
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
index 0cc4992ef6..17a8e45a35 100644
--- a/actioncable/test/channel/periodic_timers_test.rb
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -38,23 +38,26 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
test "disallow negative and zero periods" do
[ 0, 0.0, 0.seconds, -1, -1.seconds, "foo", :foo, Object.new ].each do |invalid|
- assert_raise ArgumentError, /Expected every:/ do
+ e = assert_raise ArgumentError do
ChatChannel.periodically :send_updates, every: invalid
end
+ assert_match(/Expected every:/, e.message)
end
end
test "disallow block and arg together" do
- assert_raise ArgumentError, /not both/ do
+ e = assert_raise ArgumentError do
ChatChannel.periodically(:send_updates, every: 1) { ping }
end
+ assert_match(/not both/, e.message)
end
test "disallow unknown args" do
[ "send_updates", Object.new, nil ].each do |invalid|
- assert_raise ArgumentError, /Expected a Symbol/ do
+ e = assert_raise ArgumentError do
ChatChannel.periodically invalid, every: 1
end
+ assert_match(/Expected a Symbol/, e.message)
end
end
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index 31dcde2e29..50fc7be30b 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -55,6 +55,8 @@ module ActionCable::StreamTests
channel = ChatChannel.new connection, "{id: 1}", id: 1
channel.subscribe_to_channel
+ wait_for_async
+
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
channel.unsubscribe_from_channel
end
@@ -67,6 +69,8 @@ module ActionCable::StreamTests
channel = SymbolChannel.new connection, ""
channel.subscribe_to_channel
+ wait_for_async
+
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
channel.unsubscribe_from_channel
end
diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb
index a4dfcc06f0..f3d3bc0603 100644
--- a/actioncable/test/connection/identifier_test.rb
+++ b/actioncable/test/connection/identifier_test.rb
@@ -53,7 +53,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
end
end
- protected
+ private
def open_connection_with_stubbed_pubsub
server = TestServer.new
server.stubs(:adapter).returns(stub_everything("adapter"))
diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb
index 67e68355c5..ca1a08f4d6 100644
--- a/actioncable/test/connection/multiple_identifiers_test.rb
+++ b/actioncable/test/connection/multiple_identifiers_test.rb
@@ -19,7 +19,7 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase
end
end
- protected
+ private
def open_connection_with_stubbed_pubsub
server = TestServer.new
server.stubs(:pubsub).returns(stub_everything("pubsub"))
diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb
index 87484765e5..6d53e249cb 100644
--- a/actioncable/test/connection/string_identifier_test.rb
+++ b/actioncable/test/connection/string_identifier_test.rb
@@ -21,7 +21,7 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase
end
end
- protected
+ private
def open_connection_with_stubbed_pubsub
@server = TestServer.new
@server.stubs(:pubsub).returns(stub_everything("pubsub"))
diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee
index 6b145dede8..a9e95c37f0 100644
--- a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee
+++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.coffee
@@ -21,6 +21,16 @@ TestHelpers.consumerTest = (name, options = {}, callback) ->
assert.equal clients.length, 1
assert.equal clients[0].readyState, WebSocket.OPEN
+ server.broadcastTo = (subscription, data = {}, callback) ->
+ data.identifier = subscription.identifier
+
+ if data.message_type
+ data.type = ActionCable.INTERNAL.message_types[data.message_type]
+ delete data.message_type
+
+ server.send(JSON.stringify(data))
+ TestHelpers.defer(callback)
+
done = ->
consumer.disconnect()
server.close()
diff --git a/actioncable/test/javascript/src/test_helpers/index.coffee b/actioncable/test/javascript/src/test_helpers/index.coffee
index d36524d9cc..c84cbbcb2c 100644
--- a/actioncable/test/javascript/src/test_helpers/index.coffee
+++ b/actioncable/test/javascript/src/test_helpers/index.coffee
@@ -4,5 +4,8 @@
ActionCable.TestHelpers =
testURL: "ws://cable.example.com/"
+ defer: (callback) ->
+ setTimeout(callback, 1)
+
originalWebSocket = ActionCable.WebSocket
QUnit.testDone -> ActionCable.WebSocket = originalWebSocket
diff --git a/actioncable/test/javascript/src/unit/consumer_test.coffee b/actioncable/test/javascript/src/unit/consumer_test.coffee
index cf8a592255..41445274eb 100644
--- a/actioncable/test/javascript/src/unit/consumer_test.coffee
+++ b/actioncable/test/javascript/src/unit/consumer_test.coffee
@@ -2,8 +2,11 @@
{consumerTest} = ActionCable.TestHelpers
module "ActionCable.Consumer", ->
- consumerTest "#connect", connect: false, ({consumer, server, done}) ->
- server.on("connection", done)
+ consumerTest "#connect", connect: false, ({consumer, server, assert, done}) ->
+ server.on "connection", ->
+ assert.equal consumer.connect(), false
+ done()
+
consumer.connect()
consumerTest "#disconnect", ({consumer, client, done}) ->
diff --git a/actioncable/test/javascript/src/unit/subscription_test.coffee b/actioncable/test/javascript/src/unit/subscription_test.coffee
new file mode 100644
index 0000000000..07027ed170
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/subscription_test.coffee
@@ -0,0 +1,40 @@
+{module, test} = QUnit
+{consumerTest} = ActionCable.TestHelpers
+
+module "ActionCable.Subscription", ->
+ consumerTest "#initialized callback", ({server, consumer, assert, done}) ->
+ consumer.subscriptions.create "chat",
+ initialized: ->
+ assert.ok true
+ done()
+
+ consumerTest "#connected callback", ({server, consumer, assert, done}) ->
+ subscription = consumer.subscriptions.create "chat",
+ connected: ->
+ assert.ok true
+ done()
+
+ server.broadcastTo(subscription, message_type: "confirmation")
+
+ consumerTest "#disconnected callback", ({server, consumer, assert, done}) ->
+ subscription = consumer.subscriptions.create "chat",
+ disconnected: ->
+ assert.ok true
+ done()
+
+ server.broadcastTo subscription, message_type: "confirmation", ->
+ server.close()
+
+ consumerTest "#perform", ({consumer, server, assert, done}) ->
+ subscription = consumer.subscriptions.create "chat",
+ connected: ->
+ @perform(publish: "hi")
+
+ server.on "message", (message) ->
+ data = JSON.parse(message)
+ assert.equal data.identifier, subscription.identifier
+ assert.equal data.command, "message"
+ assert.deepEqual data.data, JSON.stringify(action: { publish: "hi" })
+ done()
+
+ server.broadcastTo(subscription, message_type: "confirmation")
diff --git a/actioncable/test/javascript/src/unit/subscriptions_test.coffee b/actioncable/test/javascript/src/unit/subscriptions_test.coffee
new file mode 100644
index 0000000000..170b370e4a
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/subscriptions_test.coffee
@@ -0,0 +1,25 @@
+{module, test} = QUnit
+{consumerTest} = ActionCable.TestHelpers
+
+module "ActionCable.Subscriptions", ->
+ consumerTest "create subscription with channel string", ({consumer, server, assert, done}) ->
+ channel = "chat"
+
+ server.on "message", (message) ->
+ data = JSON.parse(message)
+ assert.equal data.command, "subscribe"
+ assert.equal data.identifier, JSON.stringify({channel})
+ done()
+
+ consumer.subscriptions.create(channel)
+
+ consumerTest "create subscription with channel object", ({consumer, server, assert, done}) ->
+ channel = channel: "chat", room: "action"
+
+ server.on "message", (message) ->
+ data = JSON.parse(message)
+ assert.equal data.command, "subscribe"
+ assert.equal data.identifier, JSON.stringify(channel)
+ done()
+
+ consumer.subscriptions.create(channel)
diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb
new file mode 100644
index 0000000000..9ad659912e
--- /dev/null
+++ b/actioncable/test/subscription_adapter/channel_prefix.rb
@@ -0,0 +1,36 @@
+require "test_helper"
+
+class ActionCable::Server::WithIndependentConfig < ActionCable::Server::Base
+ # ActionCable::Server::Base defines config as a class variable.
+ # Need config to be an instance variable here as we're testing 2 separate configs
+ def config
+ @config ||= ActionCable::Server::Configuration.new
+ end
+end
+
+module ChannelPrefixTest
+ def test_channel_prefix
+ server2 = ActionCable::Server::WithIndependentConfig.new
+ server2.config.cable = alt_cable_config
+ server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+
+ adapter_klass = server2.config.pubsub_adapter
+
+ rx_adapter2 = adapter_klass.new(server2)
+ tx_adapter2 = adapter_klass.new(server2)
+
+ subscribe_as_queue("channel") do |queue|
+ subscribe_as_queue("channel", rx_adapter2) do |queue2|
+ @tx_adapter.broadcast("channel", "hello world")
+ tx_adapter2.broadcast("channel", "hello world 2")
+
+ assert_equal "hello world", queue.pop
+ assert_equal "hello world 2", queue2.pop
+ end
+ end
+ end
+
+ def alt_cable_config
+ cable_config.merge(channel_prefix: "foo")
+ end
+end
diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb
index f316bc46ef..256458bc24 100644
--- a/actioncable/test/subscription_adapter/evented_redis_test.rb
+++ b/actioncable/test/subscription_adapter/evented_redis_test.rb
@@ -1,11 +1,15 @@
require "test_helper"
require_relative "./common"
+require_relative "./channel_prefix"
class EventedRedisAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
+ include ChannelPrefixTest
def setup
- super
+ assert_deprecated do
+ super
+ end
# em-hiredis is warning-rich
@previous_verbose, $VERBOSE = $VERBOSE, nil
@@ -23,6 +27,32 @@ class EventedRedisAdapterTest < ActionCable::TestCase
$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://127.0.0.1:6379/12" }
end
diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb
index 2ba5636656..4df5e0cbcd 100644
--- a/actioncable/test/subscription_adapter/redis_test.rb
+++ b/actioncable/test/subscription_adapter/redis_test.rb
@@ -1,8 +1,10 @@
require "test_helper"
require_relative "./common"
+require_relative "./channel_prefix"
class RedisAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
+ include ChannelPrefixTest
def cable_config
{ adapter: "redis", driver: "ruby", url: "redis://127.0.0.1:6379/12" }
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 3b9f503a0b..e488d867de 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,6 +1 @@
-* Exception handling: use `rescue_from` to handle exceptions raised by
- mailer actions, by message delivery, and by deferred delivery jobs.
-
- *Jeremy Daer*
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index 8573eb1225..ac810e86d0 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2017 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/bin/test b/actionmailer/bin/test
index 84a05bba08..a7beb14b27 100755
--- a/actionmailer/bin/test
+++ b/actionmailer/bin/test
@@ -2,5 +2,3 @@
COMPONENT_ROOT = File.expand_path("..", __dir__)
require File.expand_path("../tools/test", COMPONENT_ROOT)
-
-exit Minitest.run(ARGV)
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 668bc99435..211190560a 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2017 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 ActionMailer
autoload :DeliveryMethods
autoload :InlinePreviewInterceptor
autoload :MailHelper
+ autoload :Parameterized
autoload :Preview
autoload :Previews, "action_mailer/preview"
autoload :TestCase
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 1f5738bbab..6849f5c0f9 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -133,6 +133,9 @@ module ActionMailer
#
# config.action_mailer.default_url_options = { host: "example.com" }
#
+ # You can also define a <tt>default_url_options</tt> method on individual mailers to override these
+ # default settings per-mailer.
+ #
# By default when <tt>config.force_ssl</tt> is true, URLs generated for hosts will use the HTTPS protocol.
#
# = Sending mail
@@ -208,6 +211,19 @@ module ActionMailer
# end
# end
#
+ # You can also send attachments with html template, in this case you need to add body, attachments,
+ # and custom content type like this:
+ #
+ # class NotifierMailer < ApplicationMailer
+ # def welcome(recipient)
+ # attachments["free_book.pdf"] = File.read("path/to/file.pdf")
+ # mail(to: recipient,
+ # subject: "New account information",
+ # content_type: "text/html",
+ # body: "<html><body>Hello there</body></html>")
+ # end
+ # end
+ #
# = Inline Attachments
#
# You can also specify that a file should be displayed inline with other HTML. This is useful
@@ -275,20 +291,19 @@ module ActionMailer
# content_description: 'This is a description'
# end
#
- # Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
- # can define methods that evaluate as the message is being generated:
+ # Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
+ # so you can define methods that evaluate as the message is being generated:
#
# class NotifierMailer < ApplicationMailer
- # default 'X-Special-Header' => Proc.new { my_method }
+ # default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
#
# private
- #
# def my_method
# 'some complex call'
# end
# end
#
- # Note that the proc is evaluated right at the start of the mail message generation, so if you
+ # Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
# set something in the default hash using a proc, and then set the same thing inside of your
# mailer method, it will get overwritten by the mailer method.
#
@@ -311,7 +326,6 @@ module ActionMailer
# end
#
# private
- #
# def add_inline_attachment!
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
# end
@@ -417,10 +431,11 @@ module ActionMailer
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
#
- # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>.
+ # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+.
class Base < AbstractController::Base
include DeliveryMethods
include Rescuable
+ include Parameterized
include Previews
abstract!
@@ -544,9 +559,9 @@ module ActionMailer
end
end
- protected
+ private
- def set_payload_for_mail(payload, mail) #:nodoc:
+ def set_payload_for_mail(payload, mail)
payload[:mailer] = name
payload[:message_id] = mail.message_id
payload[:subject] = mail.subject
@@ -558,7 +573,7 @@ module ActionMailer
payload[:mail] = mail.encoded
end
- def method_missing(method_name, *args) # :nodoc:
+ def method_missing(method_name, *args)
if action_methods.include?(method_name.to_s)
MessageDelivery.new(self, method_name, *args)
else
@@ -566,10 +581,8 @@ module ActionMailer
end
end
- private
-
- def respond_to_missing?(method, include_all = false) #:nodoc:
- action_methods.include?(method.to_s)
+ def respond_to_missing?(method, include_all = false)
+ action_methods.include?(method.to_s) || super
end
end
@@ -588,7 +601,8 @@ module ActionMailer
def process(method_name, *args) #:nodoc:
payload = {
mailer: self.class.name,
- action: method_name
+ action: method_name,
+ args: args
}
ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
@@ -830,7 +844,7 @@ module ActionMailer
message
end
- protected
+ private
# Used by #mail to set the content type of the message.
#
@@ -841,7 +855,7 @@ module ActionMailer
# If there is no content type passed in via headers, and there are no
# attachments, or the message is multipart, then the default content type is
# used.
- def set_content_type(m, user_content_type, class_default)
+ def set_content_type(m, user_content_type, class_default) # :doc:
params = m.content_type_parameters || {}
case
when user_content_type.present?
@@ -863,23 +877,21 @@ module ActionMailer
# If it does not find a translation for the +subject+ under the specified scope it will default to a
# humanized version of the <tt>action_name</tt>.
# If the subject has interpolations, you can pass them through the +interpolations+ parameter.
- def default_i18n_subject(interpolations = {})
+ def default_i18n_subject(interpolations = {}) # :doc:
mailer_scope = self.class.mailer_name.tr("/", ".")
I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
end
# Emails do not support relative path links.
- def self.supports_path?
+ def self.supports_path? # :doc:
false
end
- private
-
def apply_defaults(headers)
default_values = self.class.default.map do |key, value|
[
key,
- value.is_a?(Proc) ? instance_eval(&value) : value
+ value.is_a?(Proc) ? instance_exec(&value) : value
]
end.to_h
@@ -900,15 +912,19 @@ module ActionMailer
yield(collector)
collector.responses
elsif headers[:body]
- [{
- body: headers.delete(:body),
- content_type: self.class.default[:content_type] || "text/plain"
- }]
+ collect_responses_from_text(headers)
else
collect_responses_from_templates(headers)
end
end
+ def collect_responses_from_text(headers)
+ [{
+ body: headers.delete(:body),
+ content_type: headers[:content_type] || "text/plain"
+ }]
+ end
+
def collect_responses_from_templates(headers)
templates_path = headers[:template_path] || self.class.mailer_name
templates_name = headers[:template_name] || action_name
@@ -959,7 +975,7 @@ module ActionMailer
end
def instrument_name
- "action_mailer"
+ "action_mailer".freeze
end
ActiveSupport.run_load_hooks(:action_mailer, self)
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index be98f4c65e..bcc4ef03cf 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -59,7 +59,7 @@ module ActionMailer
end
def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc:
- method ||= self.delivery_method
+ method ||= delivery_method
mail.delivery_handler = self
case method
diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb
index 7dafceef2b..f5594ef928 100644
--- a/actionmailer/lib/action_mailer/gem_version.rb
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -6,7 +6,7 @@ module ActionMailer
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
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 9087d335fa..980415afe0 100644
--- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -26,7 +26,7 @@ module ActionMailer
def transform! #:nodoc:
return message if html_part.blank?
- html_source.gsub!(PATTERN) do |match|
+ html_part.body = html_part.decoded.gsub(PATTERN) do |match|
if part = find_part(match[9..-2])
%[src="#{data_url(part)}"]
else
@@ -46,10 +46,6 @@ module ActionMailer
@html_part ||= message.html_part
end
- def html_source
- html_part.body.raw_source
- end
-
def data_url(part)
"data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}"
end
diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb
new file mode 100644
index 0000000000..3acacc1f14
--- /dev/null
+++ b/actionmailer/lib/action_mailer/parameterized.rb
@@ -0,0 +1,152 @@
+module ActionMailer
+ # Provides the option to parameterize mailers in order to share instance variable
+ # setup, processing, and common headers.
+ #
+ # Consider this example that does not use parameterization:
+ #
+ # class InvitationsMailer < ApplicationMailer
+ # def account_invitation(inviter, invitee)
+ # @account = inviter.account
+ # @inviter = inviter
+ # @invitee = invitee
+ #
+ # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
+ #
+ # mail \
+ # subject: subject,
+ # to: invitee.email_address,
+ # from: common_address(inviter),
+ # reply_to: inviter.email_address_with_name
+ # end
+ #
+ # def project_invitation(project, inviter, invitee)
+ # @account = inviter.account
+ # @project = project
+ # @inviter = inviter
+ # @invitee = invitee
+ # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
+ #
+ # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
+ #
+ # mail \
+ # subject: subject,
+ # to: invitee.email_address,
+ # from: common_address(inviter),
+ # reply_to: inviter.email_address_with_name
+ # end
+ #
+ # def bulk_project_invitation(projects, inviter, invitee)
+ # @account = inviter.account
+ # @projects = projects.sort_by(&:name)
+ # @inviter = inviter
+ # @invitee = invitee
+ #
+ # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
+ #
+ # mail \
+ # subject: subject,
+ # to: invitee.email_address,
+ # from: common_address(inviter),
+ # reply_to: inviter.email_address_with_name
+ # end
+ # end
+ #
+ # InvitationsMailer.account_invitation(person_a, person_b).deliver_later
+ #
+ # Using parameterized mailers, this can be rewritten as:
+ #
+ # 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
+ #
+ # def bulk_project_invitation
+ # @projects = params[:projects].sort_by(&:name)
+ #
+ # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
+ # end
+ # end
+ #
+ # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
+ module Parameterized
+ extend ActiveSupport::Concern
+
+ included do
+ attr_accessor :params
+ end
+
+ module ClassMethods
+ # Provide the parameters to the mailer in order to use them in the instance methods and callbacks.
+ #
+ # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
+ #
+ # See Parameterized documentation for full example.
+ def with(params)
+ ActionMailer::Parameterized::Mailer.new(self, params)
+ end
+ end
+
+ class Mailer # :nodoc:
+ def initialize(mailer, params)
+ @mailer, @params = mailer, params
+ end
+
+ private
+ def method_missing(method_name, *args)
+ if @mailer.action_methods.include?(method_name.to_s)
+ ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method, include_all = false)
+ @mailer.respond_to?(method, include_all)
+ end
+ end
+
+ class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
+ def initialize(mailer_class, action, params, *args)
+ super(mailer_class, action, *args)
+ @params = params
+ end
+
+ private
+ def processed_mailer
+ @processed_mailer ||= @mailer_class.new.tap do |mailer|
+ mailer.params = @params
+ mailer.process @action, *@args
+ end
+ end
+
+ def enqueue_delivery(delivery_method, options = {})
+ if processed?
+ super
+ else
+ args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args
+ ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args)
+ end
+ end
+ end
+
+ class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
+ def perform(mailer, mail_method, delivery_method, params, *args)
+ mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
index 93a11453bf..b0152aff03 100644
--- a/actionmailer/lib/action_mailer/preview.rb
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -63,7 +63,7 @@ module ActionMailer
# interceptors will be informed so that they can transform the message
# as they would if the mail was actually being delivered.
def call(email)
- preview = self.new
+ preview = new
message = preview.public_send(email)
inform_preview_interceptors(message)
message
@@ -94,22 +94,22 @@ module ActionMailer
name.sub(/Preview$/, "").underscore
end
- protected
- def load_previews #:nodoc:
+ private
+ def load_previews
if preview_path
Dir["#{preview_path}/**/*_preview.rb"].each { |file| require_dependency file }
end
end
- def preview_path #:nodoc:
+ def preview_path
Base.preview_path
end
- def show_previews #:nodoc:
+ def show_previews
Base.show_previews
end
- def inform_preview_interceptors(message) #:nodoc:
+ def inform_preview_interceptors(message)
Base.preview_interceptors.each do |interceptor|
interceptor.previewing_email(message)
end
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index 1d09a4ee96..9ead03a40c 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -4,8 +4,8 @@ require "rails-dom-testing"
module ActionMailer
class NonInferrableMailerError < ::StandardError
def initialize(name)
- super "Unable to determine the mailer to test from #{name}. " +
- "You'll need to specify it using tests YourMailer in your " +
+ super "Unable to determine the mailer to test from #{name}. " \
+ "You'll need to specify it using tests YourMailer in your " \
"test case definition"
end
end
@@ -57,7 +57,7 @@ module ActionMailer
end
def mailer_class
- if mailer = self._mailer_class
+ if mailer = _mailer_class
mailer
else
tests determine_default_mailer(name)
@@ -73,38 +73,36 @@ module ActionMailer
end
end
- protected
+ private
- def initialize_test_deliveries # :nodoc:
+ def initialize_test_deliveries
set_delivery_method :test
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries.clear
end
- def restore_test_deliveries # :nodoc:
+ def restore_test_deliveries
restore_delivery_method
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
end
- def set_delivery_method(method) # :nodoc:
+ def set_delivery_method(method)
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = method
end
- def restore_delivery_method # :nodoc:
+ def restore_delivery_method
ActionMailer::Base.deliveries.clear
ActionMailer::Base.delivery_method = @old_delivery_method
end
- def set_expected_mail # :nodoc:
+ def set_expected_mail
@expected = Mail.new
@expected.content_type ["text", "plain", { "charset" => charset }]
@expected.mime_version = "1.0"
end
- private
-
def charset
"UTF-8"
end
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index c17ecad4c6..c30fb1fc18 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -88,7 +88,7 @@ module ActionMailer
# end
# end
def assert_enqueued_emails(number, &block)
- assert_enqueued_jobs number, only: ActionMailer::DeliveryJob, &block
+ assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
end
# Asserts that no emails are enqueued for later delivery.
@@ -107,7 +107,7 @@ module ActionMailer
# end
# end
def assert_no_enqueued_emails(&block)
- assert_no_enqueued_jobs only: ActionMailer::DeliveryJob, &block
+ assert_no_enqueued_jobs only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block
end
end
end
diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index 9dd7ee7a27..99fe4544f1 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -11,7 +11,7 @@ module Rails
template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb")
in_root do
- if self.behavior == :invoke && !File.exist?(application_mailer_file_name)
+ if behavior == :invoke && !File.exist?(application_mailer_file_name)
template "application_mailer.rb", application_mailer_file_name
end
end
@@ -19,12 +19,11 @@ module Rails
hook_for :template_engine, :test_framework
- protected
- def file_name
+ private
+ def file_name # :doc:
@_file_name ||= super.gsub(/_mailer/i, "")
end
- private
def application_mailer_file_name
@_application_mailer_file_name ||= if mountable_engine?
"app/mailers/#{namespaced_path}/application_mailer.rb"
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 82f03f5cba..a646cbd581 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -3,8 +3,8 @@ require "active_support/core_ext/kernel/reporting"
# These are the normal settings that will be set up by Railties
# TODO: Have these tests support other combinations of these values
silence_warnings do
- Encoding.default_internal = "UTF-8"
- Encoding.default_external = "UTF-8"
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
end
module Rails
@@ -31,15 +31,15 @@ I18n.enforce_available_locales = false
FIXTURE_LOAD_PATH = File.expand_path("fixtures", File.dirname(__FILE__))
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
-end
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
-end
-
class ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ private def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ private def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 3bca69890d..61960d411d 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -140,6 +140,11 @@ class BaseTest < ActiveSupport::TestCase
assert_equal("multipart/mixed", email.mime_type)
end
+ test "set mime type to text/html when attachment is included and body is set" do
+ email = BaseMailer.attachment_with_content(body: "Hello there", content_type: "text/html")
+ assert_equal("text/html", email.mime_type)
+ end
+
test "adds the rendered template as part" do
email = BaseMailer.attachment_with_content
assert_equal(2, email.parts.length)
@@ -829,7 +834,26 @@ class BaseTest < ActiveSupport::TestCase
assert_equal "special indeed!", mail["X-Special-Header"].to_s
end
- protected
+ test "notification for process" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe("process.action_mailer") do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ BaseMailer.welcome(body: "Hello there").deliver_now
+
+ assert_equal 1, events.length
+ assert_equal "process.action_mailer", events[0].name
+ assert_equal "BaseMailer", events[0].payload[:mailer]
+ assert_equal :welcome, events[0].payload[:action]
+ assert_equal [{ body: "Hello there" }], events[0].payload[:args]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "process.action_mailer"
+ end
+ end
+
+ private
# Execute the block setting the given values and restoring old values after
# the block is executed.
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index 039685ffe5..4f09339800 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -57,16 +57,14 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance|
assert_called(instance, :deliver!) do
with_translation "de", email_subject: "[Anmeldung] Willkommen" do
- ActiveSupport::Deprecation.silence do
- get "/test/send_mail"
- end
+ get "/test/send_mail"
assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body
end
end
end
end
- protected
+ private
def with_translation(locale, data)
I18n.backend.store_translations(locale, data)
diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb
index 972629e477..6042548aef 100644
--- a/actionmailer/test/mail_helper_test.rb
+++ b/actionmailer/test/mail_helper_test.rb
@@ -2,11 +2,11 @@ require "abstract_unit"
class HelperMailer < ActionMailer::Base
def use_mail_helper
- @text = "But soft! What light through yonder window breaks? It is the east, " +
- "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " +
- "which is sick and pale with grief that thou, her maid, art far more " +
- "fair than she. Be not her maid, for she is envious! Her vestal " +
- "livery is but sick and green, and none but fools do wear it. Cast " +
+ @text = "But soft! What light through yonder window breaks? It is the east, " \
+ "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " \
+ "which is sick and pale with grief that thou, her maid, art far more " \
+ "fair than she. Be not her maid, for she is envious! Her vestal " \
+ "livery is but sick and green, and none but fools do wear it. Cast " \
"it off!"
mail_with_defaults do |format|
@@ -65,7 +65,7 @@ The second
end
end
- protected
+ private
def mail_with_defaults(&block)
mail(to: "test@localhost", from: "tester@example.com",
diff --git a/actionmailer/test/mailers/params_mailer.rb b/actionmailer/test/mailers/params_mailer.rb
new file mode 100644
index 0000000000..4c0fae6d91
--- /dev/null
+++ b/actionmailer/test/mailers/params_mailer.rb
@@ -0,0 +1,11 @@
+class ParamsMailer < ActionMailer::Base
+ before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
+
+ default to: Proc.new { @invitee }, from: -> { @inviter }
+
+ def invitation
+ mail(subject: "Welcome to the project!") do |format|
+ format.text { render plain: "So says #{@inviter}" }
+ end
+ end
+end
diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb
index a79d77e1e5..c0683be94d 100644
--- a/actionmailer/test/message_delivery_test.rb
+++ b/actionmailer/test/message_delivery_test.rb
@@ -76,14 +76,14 @@ class MessageDeliveryTest < ActiveSupport::TestCase
test "should enqueue a delivery with a delay" do
travel_to Time.new(2004, 11, 24, 01, 04, 44) do
- assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current.to_f + 600.seconds, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do
- @mail.deliver_later wait: 600.seconds
+ assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do
+ @mail.deliver_later wait: 10.minutes
end
end
end
test "should enqueue a delivery at a specific time" do
- later_time = Time.now.to_f + 3600
+ later_time = Time.current + 1.hour
assert_performed_with(job: ActionMailer::DeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do
@mail.deliver_later wait_until: later_time
end
diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb
new file mode 100644
index 0000000000..914ed12312
--- /dev/null
+++ b/actionmailer/test/parameterized_test.rb
@@ -0,0 +1,55 @@
+require "abstract_unit"
+require "active_job"
+require "mailers/params_mailer"
+
+class ParameterizedTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ setup do
+ @previous_logger = ActiveJob::Base.logger
+ ActiveJob::Base.logger = Logger.new(nil)
+
+ @previous_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+
+ @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
+ ActionMailer::Base.deliver_later_queue_name = :test_queue
+ ActionMailer::Base.delivery_method = :test
+
+ @mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation
+ end
+
+ teardown do
+ ActiveJob::Base.logger = @previous_logger
+ ParamsMailer.deliveries.clear
+
+ ActionMailer::Base.delivery_method = @previous_delivery_method
+ ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
+ end
+
+ test "parameterized headers" do
+ assert_equal(["jason@basecamp.com"], @mail.to)
+ assert_equal(["david@basecamp.com"], @mail.from)
+ assert_equal("So says david@basecamp.com", @mail.body.encoded)
+ end
+
+ test "enqueue the email with params" do
+ assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do
+ @mail.deliver_later
+ end
+ end
+
+ test "respond_to?" do
+ mailer = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com")
+
+ assert_respond_to mailer, :invitation
+ assert_not_respond_to mailer, :anything
+
+ invitation = mailer.method(:invitation)
+ assert_equal Method, invitation.class
+
+ assert_raises(NameError) do
+ invitation = mailer.method(:anything)
+ end
+ end
+end
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index 31ac5a5211..876e9b0634 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -143,6 +143,16 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_enqueued_parameterized_emails
+ assert_nothing_raised do
+ assert_enqueued_emails 1 do
+ silence_stream($stdout) do
+ TestHelperMailer.with(a: 1).test.deliver_later
+ end
+ end
+ end
+ end
+
def test_assert_enqueued_emails_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_emails 2 do
@@ -176,6 +186,14 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_no_enqueued_parameterized_emails
+ assert_nothing_raised do
+ assert_no_enqueued_emails do
+ TestHelperMailer.with(a: 1).test.deliver_now
+ end
+ end
+ end
+
def test_assert_no_enqueued_emails_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_enqueued_emails do
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 0c8625ae08..c5b679207a 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,297 +1 @@
-* Reset a new session directly after its creation in ActionDispatch::IntegrationTest#open_session.
-
- Fixes #22742.
-
- *Tawan Sierek*
-
-* Fixes incorrect output from rails routes when using singular resources.
-
- Fixes #26606.
-
- *Erick Reyna*
-
-* Fixes multiple calls to `logger.fatal` instead of a single call,
- for every line in an exception backtrace, when printing trace
- from `DebugExceptions` middleware.
-
- Fixes #26134.
-
- *Vipul A M*
-
-* Add support for arbitrary hashes in strong parameters:
-
- ```ruby
- params.permit(preferences: {})
- ```
-
- *Xavier Noria*
-
-* Add `ActionController::Parameters#merge!`, which behaves the same as `Hash#merge!`.
-
- *Yuji Yaginuma*
-
-* Allow keys not found in `RACK_KEY_TRANSLATION` for setting the environment when rendering
- arbitrary templates.
-
- *Sammy Larbi*
-
-* Remove deprecated support to non-keyword arguments in `ActionDispatch::IntegrationTest#process`,
- `#get`, `#post`, `#patch`, `#put`, `#delete`, and `#head`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `ActionDispatch::IntegrationTest#*_via_redirect`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `ActionDispatch::IntegrationTest#xml_http_request`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support for passing `:path` and route path as strings in `ActionDispatch::Routing::Mapper#match`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support for passing path as `nil` in `ActionDispatch::Routing::Mapper#match`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `cache_control` argument from `ActionDispatch::Static#initialize`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to passing strings or symbols to the middleware stack.
-
- *Rafael Mendonça França*
-
-* Change HSTS subdomain to true.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `host` and `port` ssl options.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `const_error` argument in
- `ActionDispatch::Session::SessionRestoreError#initialize`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `#original_exception` in `ActionDispatch::Session::SessionRestoreError`.
-
- *Rafael Mendonça França*
-
-* Deprecate `ActionDispatch::ParamsParser::ParseError` in favor of
- `ActionDispatch::Http::Parameters::ParseError`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `ActionDispatch::ParamsParser`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `original_exception` and `message` arguments in
- `ActionDispatch::ParamsParser::ParseError#initialize`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `#original_exception` in `ActionDispatch::ParamsParser::ParseError`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated access to mime types through constants.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to non-keyword arguments in `ActionController::TestCase#process`,
- `#get`, `#post`, `#patch`, `#put`, `#delete`, and `#head`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `xml_http_request` and `xhr` methods in `ActionController::TestCase`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated methods in `ActionController::Parameters`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to comparing a `ActionController::Parameters`
- with a `Hash`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to `:text` in `render`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to `:nothing` in `render`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to `:back` in `redirect_to`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to passing status as option `head`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated support to passing original exception to `ActionController::BadRequest`
- and the `ActionController::BadRequest#original_exception` method.
-
- *Rafael Mendonça França*
-
-* Remove deprecated methods `skip_action_callback`, `skip_filter`, `before_filter`,
- `prepend_before_filter`, `skip_before_filter`, `append_before_filter`, `around_filter`
- `prepend_around_filter`, `skip_around_filter`, `append_around_filter`, `after_filter`,
- `prepend_after_filter`, `skip_after_filter` and `append_after_filter`.
-
- *Rafael Mendonça França*
-
-* Show an "unmatched constraints" error when params fail to match constraints
- on a matched route, rather than a "missing keys" error.
-
- Fixes #26470.
-
- *Chris Carter*
-
-* Fix adding implicitly rendered template digests to ETags.
-
- Fixes a case when modifying an implicitly rendered template for a
- controller action using `fresh_when` or `stale?` would not result in a new
- `ETag` value.
-
- *Javan Makhmali*
-
-* Make `fixture_file_upload` work in integration tests.
-
- *Yuji Yaginuma*
-
-* Add `to_param` to `ActionController::Parameters` deprecations.
-
- In the future `ActionController::Parameters` are discouraged from being used
- in URLs without explicit whitelisting. Go through `to_h` to use `to_param`.
-
- *Kir Shatrov*
-
-* Fix nested multiple roots
-
- The PR #20940 enabled the use of multiple roots with different constraints
- at the top level but unfortunately didn't work when those roots were inside
- a namespace and also broke the use of root inside a namespace after a top
- level root was defined because the check for the existence of the named route
- used the global :root name and not the namespaced name.
-
- This is fixed by using the name_for_action method to expand the :root name to
- the full namespaced name. We can pass nil for the second argument as we're not
- dealing with resource definitions so don't need to handle the cases for edit
- and new routes.
-
- Fixes #26148.
-
- *Ryo Hashimoto*, *Andrew White*
-
-* Include the content of the flash in the auto-generated etag. This solves the following problem:
-
- 1. POST /messages
- 2. redirect_to messages_url, notice: 'Message was created'
- 3. GET /messages/1
- 4. GET /messages
-
- Step 4 would before still include the flash message, even though it's no longer relevant,
- because the etag cache was recorded with the flash in place and didn't change when it was gone.
-
- *DHH*
-
-* SSL: Changes redirect behavior for all non-GET and non-HEAD requests
- (like POST/PUT/PATCH etc) to `http://` resources to redirect to `https://`
- with a [307 status code](http://tools.ietf.org/html/rfc7231#section-6.4.7) instead of [301 status code](http://tools.ietf.org/html/rfc7231#section-6.4.2).
-
- 307 status code instructs the HTTP clients to preserve the original
- request method while redirecting. It has been part of HTTP RFC since
- 1999 and is implemented/recognized by most (if not all) user agents.
-
- # Before
- POST http://example.com/articles (i.e. ArticlesContoller#create)
- redirects to
- GET https://example.com/articles (i.e. ArticlesContoller#index)
-
- # After
- POST http://example.com/articles (i.e. ArticlesContoller#create)
- redirects to
- POST https://example.com/articles (i.e. ArticlesContoller#create)
-
- *Chirag Singhal*
-
-* Add `:as` option to `ActionController:TestCase#process` and related methods.
-
- Specifying `as: mime_type` allows the `CONTENT_TYPE` header to be specified
- in controller tests without manually doing this through `@request.headers['CONTENT_TYPE']`.
-
- *Everest Stefan Munro-Zeisberger*
-
-* Show cache hits and misses when rendering partials.
-
- Partials using the `cache` helper will show whether a render hit or missed
- the cache:
-
- ```
- Rendered messages/_message.html.erb in 1.2 ms [cache hit]
- Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss]
- ```
-
- This removes the need for the old fragment cache logging:
-
- ```
- Read fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/d0bdf2974e1ef6d31685c3b392ad0b74 (0.6ms)
- Rendered messages/_message.html.erb in 1.2 ms [cache hit]
- Write fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/3b4e249ac9d168c617e32e84b99218b5 (1.1ms)
- Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss]
- ```
-
- Though that full output can be reenabled with
- `config.action_controller.enable_fragment_cache_logging = true`.
-
- *Stan Lo*
-
-* Don't override the `Accept` header in integration tests when called with `xhr: true`.
-
- Fixes #25859.
-
- *David Chen*
-
-* Fix `defaults` option for root route.
-
- A regression from some refactoring for the 5.0 release, this change
- fixes the use of `defaults` (default parameters) in the `root` routing method.
-
- *Chris Arcand*
-
-* Check `request.path_parameters` encoding at the point they're set.
-
- Check for any non-UTF8 characters in path parameters at the point they're
- set in `env`. Previously they were checked for when used to get a controller
- class, but this meant routes that went directly to a Rack app, or skipped
- controller instantiation for some other reason, had to defend against
- non-UTF8 characters themselves.
-
- *Grey Baker*
-
-* Don't raise `ActionController::UnknownHttpMethod` from `ActionDispatch::Static`.
-
- Pass `Rack::Request` objects to `ActionDispatch::FileHandler` to avoid it
- raising `ActionController::UnknownHttpMethod`. If an unknown method is
- passed, it should pass exception higher in the stack instead, once we've had a
- chance to define exception handling behaviour.
-
- *Grey Baker*
-
-* Handle `Rack::QueryParser` errors in `ActionDispatch::ExceptionWrapper`.
-
- Updated `ActionDispatch::ExceptionWrapper` to handle the Rack 2.0 namespace
- for `ParameterTypeError` and `InvalidParameterError` errors.
-
- *Grey Baker*
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
index 8573eb1225..ac810e86d0 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2017 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/bin/test b/actionpack/bin/test
index 84a05bba08..a7beb14b27 100755
--- a/actionpack/bin/test
+++ b/actionpack/bin/test
@@ -2,5 +2,3 @@
COMPONENT_ROOT = File.expand_path("..", __dir__)
require File.expand_path("../tools/test", COMPONENT_ROOT)
-
-exit Minitest.run(ARGV)
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index 603c2e9ea7..e7cb6347a2 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,4 +1,3 @@
-require "erubis"
require "abstract_controller/error"
require "active_support/configurable"
require "active_support/descendants_tracker"
@@ -22,7 +21,6 @@ module AbstractController
include ActiveSupport::Configurable
extend ActiveSupport::DescendantsTracker
- undef_method :not_implemented
class << self
attr_reader :abstract
alias_method :abstract?, :abstract
diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb
index d222880922..26e3f08bc1 100644
--- a/actionpack/lib/abstract_controller/caching.rb
+++ b/actionpack/lib/abstract_controller/caching.rb
@@ -52,9 +52,9 @@ module AbstractController
self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
end
- protected
+ private
# Convenience accessor.
- def cache(key, options = {}, &block)
+ def cache(key, options = {}, &block) # :doc:
if cache_configured?
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
else
diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb
index 57714b0588..40ae5aa1ca 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -19,7 +19,7 @@ module AbstractController
generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym)
end
- protected
+ private
def method_missing(symbol, &block)
unless mime_constant = Mime[symbol]
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index d339580435..54af938a93 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -109,6 +109,9 @@ module AbstractController
def _process_format(format)
end
+ def _process_variant(options)
+ end
+
def _set_html_content_type # :nodoc:
end
@@ -119,10 +122,7 @@ module AbstractController
# :api: private
def _normalize_render(*args, &block)
options = _normalize_args(*args, &block)
- #TODO: remove defined? when we restore AP <=> AV dependency
- if defined?(request) && !request.nil? && request.variant.present?
- options[:variant] = request.variant
- end
+ _process_variant(options)
_normalize_options(options)
options
end
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index 5cd8d77ddb..0d1af0d0bd 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -81,10 +81,9 @@ module ActionController
# end
# end
#
- # Quite straightforward. Make sure to check the modules included in
- # <tt>ActionController::Base</tt> if you want to use any other
- # functionality that is not provided by <tt>ActionController::API</tt>
- # out of the box.
+ # Make sure to check the modules included in <tt>ActionController::Base</tt>
+ # if you want to use any other functionality that is not provided
+ # by <tt>ActionController::API</tt> out of the box.
class API < Metal
abstract!
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index ca8066cd82..0fe0853da3 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -8,7 +8,7 @@ module ActionController
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
#
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
- # controllers in turn inherit from ApplicationController. This gives you one class to configure things such as
+ # controllers inherit from ApplicationController. This gives you one class to configure things such as
# request forgery protection and filtering of sensitive request parameters.
#
# A sample controller could look like this:
@@ -30,7 +30,7 @@ module ActionController
#
# Unlike index, the create action will not render a template. After performing its main purpose (creating a
# new post), it initiates a redirect instead. This redirect works by returning an external
- # "302 Moved" HTTP response that takes the user to the index action.
+ # <tt>302 Moved</tt> HTTP response that takes the user to the index action.
#
# These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect.
# Most actions are variations on these themes.
@@ -59,7 +59,7 @@ module ActionController
# <input type="text" name="post[name]" value="david">
# <input type="text" name="post[address]" value="hyacintvej">
#
- # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
+ # A request coming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
# If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
#
@@ -74,7 +74,7 @@ module ActionController
#
# session[:person] = Person.authenticate(user_name, password)
#
- # And retrieved again through the same hash:
+ # You can retrieve it again through the same hash:
#
# Hello #{session[:person]}
#
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index a9a8508abc..954265ad97 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -38,7 +38,7 @@ module ActionController
end
def instrument_name
- "action_controller"
+ "action_controller".freeze
end
end
end
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index ed93a2f09c..74c4153cd2 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -118,11 +118,6 @@ module ActionController
class Metal < AbstractController::Base
abstract!
- def env
- @_request.env
- end
- deprecate :env
-
# Returns the last part of the controller's name, underscored, without the ending
# <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
# Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
@@ -139,11 +134,11 @@ module ActionController
end
end
- def self.encoding_for_param(action, param) # :nodoc:
- ::Encoding::UTF_8
+ def self.binary_params_for?(action) # :nodoc:
+ false
end
- # Delegates to the class' <tt>controller_name</tt>
+ # Delegates to the class' <tt>controller_name</tt>.
def controller_name
self.class.controller_name
end
@@ -232,14 +227,6 @@ module ActionController
middleware_stack
end
- # Makes the controller a Rack endpoint that runs the action in the given
- # +env+'s +action_dispatch.request.path_parameters+ key.
- def self.call(env)
- req = ActionDispatch::Request.new env
- action(req.path_parameters[:action]).call(env)
- end
- class << self; deprecate :call; end
-
# Returns a Rack endpoint for the given action name.
def self.action(name)
if middleware_stack.any?
@@ -257,7 +244,7 @@ module ActionController
end
end
- # Direct dispatch to the controller. Instantiates the controller, then
+ # Direct dispatch to the controller. Instantiates the controller, then
# executes the action named +name+.
def self.dispatch(name, req, res)
if middleware_stack.any?
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index f089c8423b..731e03e2fc 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -11,7 +11,7 @@ module ActionController #:nodoc:
DEFAULT_SEND_FILE_TYPE = "application/octet-stream".freeze #:nodoc:
DEFAULT_SEND_FILE_DISPOSITION = "attachment".freeze #:nodoc:
- protected
+ private
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
# via the Rack::Sendfile middleware. The header to use is set via
# +config.action_dispatch.x_sendfile_header+.
@@ -70,7 +70,6 @@ module ActionController #:nodoc:
send_file_headers! options
self.status = options[:status] || 200
- self.content_type = options[:type] if options.key?(:type)
self.content_type = options[:content_type] if options.key?(:content_type)
response.send_file path
end
@@ -109,10 +108,12 @@ module ActionController #:nodoc:
render options.slice(:status, :content_type).merge(body: data)
end
- private
def send_file_headers!(options)
type_provided = options.has_key?(:type)
+ self.content_type = DEFAULT_SEND_FILE_TYPE
+ response.sending_file = true
+
content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
raise ArgumentError, ":type option required" if content_type.nil?
@@ -137,8 +138,6 @@ module ActionController #:nodoc:
headers["Content-Transfer-Encoding"] = "binary"
- response.sending_file = true
-
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb
index 474d75f02e..7bd338bd7c 100644
--- a/actionpack/lib/action_controller/metal/etag_with_flash.rb
+++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb
@@ -1,9 +1,9 @@
module ActionController
# When you're using the flash, it's generally used as a conditional on the view.
# This means the content of the view depends on the flash. Which in turn means
- # that the etag for a response should be computed with the content of the flash
+ # that the ETag for a response should be computed with the content of the flash
# in mind. This does that by including the content of the flash as a component
- # in the etag that's generated for a response.
+ # in the ETag that's generated for a response.
module EtagWithFlash
extend ActiveSupport::Concern
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb
index 65351284b9..347fbf0e74 100644
--- a/actionpack/lib/action_controller/metal/flash.rb
+++ b/actionpack/lib/action_controller/metal/flash.rb
@@ -42,7 +42,7 @@ module ActionController #:nodoc:
end
end
- protected
+ private
def redirect_to(options = {}, response_status_and_flash = {}) #:doc:
self.class._flash_types.each do |flash_type|
if type = response_status_and_flash.delete(flash_type)
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index b8976497a4..73e67573ca 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -2,17 +2,17 @@ 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 HTTPS
- # protocol. This will ensure that user's sensitive information will be
+ # 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 transferred via HTTPS protocol and prevent
- # the user from getting their session hijacked when accessing the site over
- # unsecured HTTP protocol.
+ # 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
extend ActiveSupport::Concern
include AbstractController::Callbacks
@@ -23,7 +23,7 @@ module ActionController
module ClassMethods
# Force the request to this particular controller or specified actions to be
- # under HTTPS protocol.
+ # 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.
@@ -71,7 +71,7 @@ module ActionController
# Redirect the existing request to use the HTTPS protocol.
#
# ==== Parameters
- # * <tt>host_or_options</tt> - Either a host name or any of the url &
+ # * <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?
@@ -89,7 +89,7 @@ module ActionController
end
secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
- flash.keep if respond_to?(:flash)
+ flash.keep if respond_to?(:flash) && request.respond_to?(:flash)
redirect_to secure_url, options.slice(*REDIRECT_OPTIONS)
end
end
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 4dff23dd85..0c50894bce 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -37,7 +37,7 @@ module ActionController
if include_content?(response_code)
self.content_type = content_type || (Mime[formats.first] if formats)
- self.response.charset = false
+ response.charset = false
end
true
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 5bf0a99fe4..d8bc895265 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -28,7 +28,7 @@ module ActionController
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
- # protected
+ # private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
@@ -363,7 +363,7 @@ module ActionController
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
- # protected
+ # private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
@@ -445,7 +445,7 @@ module ActionController
end
end
- # Parses the token and options out of the token authorization header.
+ # Parses the token and options out of the token Authorization header.
# The value for the Authorization header is expected to have the prefix
# <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
# Authorization: Token token="abc", nonce="def"
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 8615c16c6f..eeb27f99f4 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,14 +1,12 @@
-require "active_support/core_ext/string/strip"
-
module ActionController
# Handles implicit rendering for a controller action that does not
# explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
#
- # For API controllers, the implicit response is always 204 No Content.
+ # For API controllers, the implicit response is always <tt>204 No Content</tt>.
#
# For all other controllers, we use these heuristics to decide whether to
# render a template, raise an error for a missing template, or respond with
- # 204 No Content:
+ # <tt>204 No Content</tt>:
#
# First, if we DO find a template, it's rendered. Template lookup accounts
# for the action name, locales, format, variant, template handlers, and more
@@ -25,7 +23,7 @@ module ActionController
# <tt>ActionView::UnknownFormat</tt> with an explanation.
#
# Finally, if we DON'T find a template AND the request isn't a browser page
- # load, then we implicitly respond with 204 No Content.
+ # load, then we implicitly respond with <tt>204 No Content</tt>.
module ImplicitRender
# :stopdoc:
include BasicImplicitRender
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index f83396ae55..2485d27cec 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -3,7 +3,7 @@ require "abstract_controller/logger"
module ActionController
# Adds instrumentation to several ends in ActionController::Base. It also provides
- # some hooks related with process_action, this allows an ORM like Active Record
+ # some hooks related with process_action. This allows an ORM like Active Record
# and/or DataMapper to plug in ActionController and show related information.
#
# Check ActiveRecord::Railties::ControllerRuntime for an example.
@@ -83,14 +83,14 @@ module ActionController
# end
#
# :api: plugin
- def cleanup_view_runtime #:nodoc:
+ def cleanup_view_runtime
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) #:nodoc:
+ def append_info_to_payload(payload)
payload[:view_runtime] = view_runtime
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index fed99e6c82..a607ee2309 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -239,8 +239,8 @@ module ActionController
error = nil
# This processes the action in a child thread. It lets us return the
- # response code and headers back up the rack stack, and still process
- # the body in parallel with sending data to the client
+ # response code and headers back up the Rack stack, and still process
+ # the body in parallel with sending data to the client.
new_controller_thread {
ActiveSupport::Dependencies.interlock.running do
t2 = Thread.current
@@ -278,9 +278,9 @@ module ActionController
raise error if error
end
- # Spawn a new thread to serve up the controller in. This is to get
+ # Spawn a new thread to serve up the controller in. This is to get
# around the fact that Rack isn't based around IOs and we need to use
- # a thread to stream data from the response bodies. Nobody should call
+ # a thread to stream data from the response bodies. Nobody should call
# this method except in Rails internals. Seriously!
def new_controller_thread # :nodoc:
Thread.new {
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index f6aabcb102..96bd548268 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -181,8 +181,8 @@ module ActionController #:nodoc:
#
# request.variant = [:tablet, :phone]
#
- # which will work similarly to formats and MIME types negotiation. If there will be no
- # +:tablet+ variant declared, +:phone+ variant will be picked:
+ # This will work similarly to formats and MIME types negotiation. If there
+ # is no +:tablet+ variant declared, the +:phone+ variant will be used:
#
# respond_to do |format|
# format.html.none
diff --git a/actionpack/lib/action_controller/metal/parameter_encoding.rb b/actionpack/lib/action_controller/metal/parameter_encoding.rb
index c457fd0d06..ecc691619e 100644
--- a/actionpack/lib/action_controller/metal/parameter_encoding.rb
+++ b/actionpack/lib/action_controller/metal/parameter_encoding.rb
@@ -1,5 +1,5 @@
module ActionController
- # Allows encoding to be specified per parameter per action.
+ # Specify binary encoding for parameters for a given action.
module ParameterEncoding
extend ActiveSupport::Concern
@@ -13,17 +13,36 @@ module ActionController
@_parameter_encodings = {}
end
- def encoding_for_param(action, param) # :nodoc:
- if @_parameter_encodings[action.to_s] && @_parameter_encodings[action.to_s][param.to_s]
- @_parameter_encodings[action.to_s][param.to_s]
- else
- super
- end
+ def binary_params_for?(action) # :nodoc:
+ @_parameter_encodings[action.to_s]
end
- def parameter_encoding(action, param_name, encoding)
- @_parameter_encodings[action.to_s] ||= {}
- @_parameter_encodings[action.to_s][param_name.to_s] = encoding
+ # Specify that a given action's parameters should all be encoded as
+ # ASCII-8BIT (it "skips" the encoding default of UTF-8).
+ #
+ # For example, a controller would use it like this:
+ #
+ # class RepositoryController < ActionController::Base
+ # skip_parameter_encoding :show
+ #
+ # def show
+ # @repo = Repository.find_by_filesystem_path params[:file_path]
+ #
+ # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
+ # # tag it as such
+ # @repo_name = params[:repo_name].force_encoding 'UTF-8'
+ # end
+ #
+ # def index
+ # @repositories = Repository.all
+ # end
+ # end
+ #
+ # The show action in the above controller would have all parameter values
+ # encoded as ASCII-8BIT. This is useful in the case where an application
+ # must handle data but encoding of the data is unknown, like file system data.
+ def skip_parameter_encoding(action)
+ @_parameter_encodings[action.to_s] = true
end
end
end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index 86e817fe16..a89fc1678b 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -105,7 +105,11 @@ module ActionController
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
- self.include = m.attribute_names
+ if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
+ self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s)
+ else
+ self.include = m.attribute_names
+ end
end
end
end
@@ -135,7 +139,7 @@ module ActionController
#
# This method also does namespace lookup. Foo::Bar::UsersController will
# try to find Foo::Bar::User, Foo::User and finally User.
- def _default_wrap_model #:nodoc:
+ def _default_wrap_model
return nil if klass.anonymous?
model_name = klass.name.sub(/Controller$/, "").classify
@@ -213,7 +217,7 @@ module ActionController
end
# Sets the default wrapper key or model which will be used to determine
- # wrapper key and attribute names. Will be called automatically when the
+ # wrapper key and attribute names. Called automatically when the
# module is inherited.
def inherited(klass)
if klass._wrapper_options.format.any?
@@ -225,7 +229,7 @@ module ActionController
end
end
- # Performs parameters wrapping upon the request. Will be called automatically
+ # Performs parameters wrapping upon the request. Called automatically
# by the metal call stack.
def process_action(*args)
if _wrapper_enabled?
@@ -238,11 +242,11 @@ module ActionController
wrapped_keys = request.request_parameters.keys
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
- # This will make the wrapped hash accessible from controller and view
+ # This will make the wrapped hash accessible from controller and view.
request.parameters.merge! wrapped_hash
request.request_parameters.merge! wrapped_hash
- # This will display the wrapped hash in the log file
+ # This will display the wrapped hash in the log file.
request.filtered_parameters.merge! wrapped_filtered_hash
end
super
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index 30798c1d99..fdfe82f96b 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -22,7 +22,7 @@ module ActionController
# redirect_to posts_url
# redirect_to proc { edit_post_url(@post) }
#
- # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option:
+ # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
#
# redirect_to post_url(@post), status: :found
# redirect_to action: 'atom', status: :moved_permanently
@@ -36,7 +36,7 @@ module ActionController
# If you are using XHR requests other than GET or POST and redirecting after the
# request then some browsers will follow the redirect using the original request
# method. This may lead to undesirable behavior such as a double DELETE. To work
- # around this you can return a <tt>303 See Other</tt> status code which will be
+ # around this you can return a <tt>303 See Other</tt> status code which will be
# followed using a GET request.
#
# redirect_to posts_url, status: :see_other
@@ -50,13 +50,16 @@ module ActionController
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
- def redirect_to(options = {}, response_status = {}) #:doc:
+ # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
+ # To terminate the execution of the function immediately after the +redirect_to+, use return.
+ # redirect_to post_url(@post) and return
+ def redirect_to(options = {}, response_status = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_status)
self.location = _compute_redirect_to_location(request, options)
- self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
+ self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end
# Redirects the browser to the page that issued the request (the referrer)
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index f8a037189c..733aca195d 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -104,7 +104,7 @@ module ActionController
#
# Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
# must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
- # and <tt>ActionController::Renderers</tt>, and have at lest one renderer.
+ # and <tt>ActionController::Renderers</tt>, and have at least one renderer.
#
# Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
# you may specify which renderers to include by passing the renderer name or names to
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index e971917ca2..67f207afc2 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -36,7 +36,7 @@ module ActionController
super
end
- # Overwrite render_to_string because body can now be set to a rack body.
+ # Overwrite render_to_string because body can now be set to a Rack body.
def render_to_string(*)
result = super
if result.respond_to?(:each)
@@ -54,6 +54,12 @@ module ActionController
private
+ def _process_variant(options)
+ if defined?(request) && !request.nil? && request.variant.present?
+ options[:variant] = request.variant
+ end
+ end
+
def _render_in_priorities(options)
RENDER_FORMATS_IN_PRIORITY.each do |format|
return options[format] if options.key?(format)
@@ -67,20 +73,20 @@ module ActionController
end
def _set_rendered_content_type(format)
- unless response.content_type
+ if format && !response.content_type
self.content_type = format.to_s
end
end
# Normalize arguments by catching blocks and setting them on :update.
- def _normalize_args(action = nil, options = {}, &blk) #:nodoc:
+ def _normalize_args(action = nil, options = {}, &blk)
options = super
options[:update] = blk if block_given?
options
end
# Normalize both text and status options.
- def _normalize_options(options) #:nodoc:
+ def _normalize_options(options)
_normalize_text(options)
if options[:html]
@@ -103,12 +109,12 @@ module ActionController
end
# Process controller specific options, as status, content-type and location.
- def _process_options(options) #:nodoc:
+ def _process_options(options)
status, content_type, location = options.values_at(:status, :content_type, :location)
self.status = status if status
self.content_type = content_type if content_type
- self.headers["Location"] = url_for(location) if location
+ headers["Location"] = url_for(location) if location
super
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 3d3c121280..d9a8b9c12d 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -152,7 +152,7 @@ module ActionController #:nodoc:
request.cookie_jar = NullCookieJar.build(request, {})
end
- protected
+ private
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
def initialize(req)
@@ -197,7 +197,7 @@ module ActionController #:nodoc:
end
end
- protected
+ private
# 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
@@ -208,7 +208,7 @@ module ActionController #:nodoc:
# enabled on an action, this before_action flags its after_action to
# verify that JavaScript responses are for XHR requests, ensuring they
# follow the browser's same-origin policy.
- def verify_authenticity_token
+ def verify_authenticity_token # :doc:
mark_for_same_origin_verification!
if !verified_request?
@@ -219,7 +219,7 @@ module ActionController #:nodoc:
end
end
- def handle_unverified_request
+ def handle_unverified_request # :doc:
forgery_protection_strategy.new(self).handle_unverified_request
end
@@ -233,7 +233,7 @@ module ActionController #:nodoc:
# 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
+ def verify_same_origin_request # :doc:
if marked_for_same_origin_verification? && non_xhr_javascript_response?
if logger && log_warning_on_csrf_failure
logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
@@ -243,18 +243,18 @@ module ActionController #:nodoc:
end
# GET requests are checked for cross-origin JavaScript after rendering.
- def mark_for_same_origin_verification!
+ def mark_for_same_origin_verification! # :doc:
@marked_for_same_origin_verification = request.get?
end
# 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?
+ def marked_for_same_origin_verification? # :doc:
@marked_for_same_origin_verification ||= false
end
# Check for cross-origin JavaScript responses.
- def non_xhr_javascript_response?
+ def non_xhr_javascript_response? # :doc:
content_type =~ %r(\Atext/javascript) && !request.xhr?
end
@@ -262,23 +262,23 @@ module ActionController #:nodoc:
# Returns true or false if a request is verified. Checks:
#
- # * Is it a GET or HEAD request? Gets should be safe and idempotent
+ # * Is it a GET or HEAD request? GETs should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params?
- # * Does the X-CSRF-Token header match the form_authenticity_token
- def verified_request?
+ # * Does the X-CSRF-Token header match the form_authenticity_token?
+ def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?)
end
# Checks if any of the authenticity tokens from the request are valid.
- def any_authenticity_token_valid?
+ def any_authenticity_token_valid? # :doc:
request_authenticity_tokens.any? do |token|
valid_authenticity_token?(session, token)
end
end
# Possible authenticity tokens sent in the request.
- def request_authenticity_tokens
+ def request_authenticity_tokens # :doc:
[form_authenticity_param, request.x_csrf_token]
end
@@ -290,7 +290,7 @@ module ActionController #:nodoc:
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
- def masked_authenticity_token(session, form_options: {})
+ def masked_authenticity_token(session, form_options: {}) # :doc:
action, method = form_options.values_at(:action, :method)
raw_token = if per_form_csrf_tokens && action && method
@@ -309,7 +309,7 @@ module ActionController #:nodoc:
# Checks the client's masked token to see if it matches the
# session token. Essentially the inverse of
# +masked_authenticity_token+.
- def valid_authenticity_token?(session, encoded_masked_token)
+ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
return false
end
@@ -327,7 +327,7 @@ module ActionController #:nodoc:
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
# This is actually an unmasked token. This is expected if
# you have just upgraded to masked tokens, but should stop
- # happening shortly after installing this gem
+ # happening shortly after installing this gem.
compare_with_real_token masked_token, session
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
@@ -336,23 +336,23 @@ module ActionController #:nodoc:
compare_with_real_token(csrf_token, session) ||
valid_per_form_csrf_token?(csrf_token, session)
else
- false # Token is malformed
+ false # Token is malformed.
end
end
- def unmask_token(masked_token)
+ def unmask_token(masked_token) # :doc:
# Split the token into the one-time pad and the encrypted
- # value and decrypt it
+ # value and decrypt it.
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token)
end
- def compare_with_real_token(token, session)
+ def compare_with_real_token(token, session) # :doc:
ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end
- def valid_per_form_csrf_token?(token, session)
+ def valid_per_form_csrf_token?(token, session) # :doc:
if per_form_csrf_tokens
correct_token = per_form_csrf_token(
session,
@@ -366,12 +366,12 @@ module ActionController #:nodoc:
end
end
- def real_csrf_token(session)
+ def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
- def per_form_csrf_token(session, action_path, method)
+ def per_form_csrf_token(session, action_path, method) # :doc:
OpenSSL::HMAC.digest(
OpenSSL::Digest::SHA256.new,
real_csrf_token(session),
@@ -379,25 +379,25 @@ module ActionController #:nodoc:
)
end
- def xor_byte_strings(s1, s2)
+ def xor_byte_strings(s1, s2) # :doc:
s2_bytes = s2.bytes
s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
s2_bytes.pack("C*")
end
# The form's authenticity parameter. Override to provide your own.
- def form_authenticity_param
+ def form_authenticity_param # :doc:
params[request_forgery_protection_token]
end
# Checks if the controller allows forgery protection.
- def protect_against_forgery?
+ def protect_against_forgery? # :doc:
allow_forgery_protection
end
# Checks if the request originated from the same origin by looking at the
# Origin header.
- def valid_request_origin?
+ def valid_request_origin? # :doc:
if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it.
request.origin.nil? || request.origin == request.base_url
@@ -406,7 +406,7 @@ module ActionController #:nodoc:
end
end
- def normalize_action_path(action_path)
+ def normalize_action_path(action_path) # :doc:
uri = URI.parse(action_path)
uri.path.chomp("/")
end
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
index 2d99e4045b..25757938f5 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -10,7 +10,7 @@ module ActionController #:nodoc:
# exceptions must be shown. This method is only called when
# consider_all_requests_local is false. By default, it returns
# false, but someone may set it to `request.local?` so local
- # requests in production still shows the detailed exception pages.
+ # requests in production still show the detailed exception pages.
def show_detailed_exceptions?
false
end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 481f19f1ef..58cf60ad2a 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -3,7 +3,7 @@ require "rack/chunked"
module ActionController #:nodoc:
# Allows views to be streamed back to the client as they are rendered.
#
- # The default way Rails renders views is by first rendering the template
+ # By default, Rails renders views by first rendering the template
# and then the layout. The response is sent to the client after the whole
# template is rendered, all queries are made, and the layout is processed.
#
@@ -193,10 +193,10 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- protected
+ private
# Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
+ def _process_options(options)
super
if options[:stream]
if request.version == "HTTP/1.0"
@@ -210,7 +210,7 @@ module ActionController #:nodoc:
end
# Call render_body if we are streaming instead of usual +render+.
- def _render_template(options) #:nodoc:
+ def _render_template(options)
if options.delete(:stream)
Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
else
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index e14da22e01..1190e0ed69 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -71,8 +71,8 @@ module ActionController
# * +permit_all_parameters+ - If it's +true+, all the parameters will be
# permitted by default. The default is +false+.
# * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters
- # that are not explicitly permitted are found. The values can be <tt>:log</tt> to
- # write a message on the logger or <tt>:raise</tt> to raise
+ # that are not explicitly permitted are found. The values can be +false+ to just filter them
+ # out, <tt>:log</tt> to additionally write a message on the logger, or <tt>:raise</tt> to raise
# ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt>
# in test and development environments, +false+ otherwise.
#
@@ -112,6 +112,77 @@ module ActionController
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
+ ##
+ # :method: as_json
+ #
+ # :call-seq:
+ # as_json(options=nil)
+ #
+ # Returns a hash that can be used as the JSON representation for the parameters.
+
+ ##
+ # :method: empty?
+ #
+ # :call-seq:
+ # empty?()
+ #
+ # Returns true if the parameters have no key/value pairs.
+
+ ##
+ # :method: has_key?
+ #
+ # :call-seq:
+ # has_key?(key)
+ #
+ # Returns true if the given key is present in the parameters.
+
+ ##
+ # :method: has_value?
+ #
+ # :call-seq:
+ # has_value?(value)
+ #
+ # Returns true if the given value is present for some key in the parameters.
+
+ ##
+ # :method: include?
+ #
+ # :call-seq:
+ # include?(key)
+ #
+ # Returns true if the given key is present in the parameters.
+
+ ##
+ # :method: key?
+ #
+ # :call-seq:
+ # key?(key)
+ #
+ # Returns true if the given key is present in the parameters.
+
+ ##
+ # :method: keys
+ #
+ # :call-seq:
+ # keys()
+ #
+ # Returns a new array of the keys of the parameters.
+
+ ##
+ # :method: value?
+ #
+ # :call-seq:
+ # value?(value)
+ #
+ # Returns true if the given value is present for some key in the parameters.
+
+ ##
+ # :method: values
+ #
+ # :call-seq:
+ # values()
+ #
+ # Returns a new array of the values of the parameters.
delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?,
:as_json, to: :@parameters
@@ -150,7 +221,7 @@ module ActionController
# permitted flag.
def ==(other)
if other.respond_to?(:permitted?)
- self.permitted? == other.permitted? && self.parameters == other.parameters
+ permitted? == other.permitted? && parameters == other.parameters
else
@parameters == other
end
@@ -191,7 +262,7 @@ module ActionController
alias_method :to_unsafe_hash, :to_unsafe_h
# Convert all hashes in values into parameters, then yield each pair in
- # the same way as <tt>Hash#each_pair</tt>
+ # 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)
@@ -339,7 +410,7 @@ module ActionController
#
# params.permit(preferences: {})
#
- # but be careful because this opens the door to arbitrary input. In this
+ # Be careful because this opens the door to arbitrary input. In this
# case, +permit+ ensures values in the returned structure are permitted
# scalars and filters out anything else.
#
@@ -398,8 +469,7 @@ module ActionController
unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
- params.permitted = true
- params
+ params.permit!
end
# Returns a parameter for the given +key+. If not found,
@@ -576,20 +646,35 @@ module ActionController
end
# Returns a new <tt>ActionController::Parameters</tt> with all keys from
- # +other_hash+ merges into current hash.
+ # +other_hash+ merged into current hash.
def merge(other_hash)
new_instance_with_inherited_permitted_status(
@parameters.merge(other_hash.to_h)
)
end
- # Returns current <tt>ActionController::Parameters</tt> instance which
- # +other_hash+ merges into current hash.
+ # Returns current <tt>ActionController::Parameters</tt> instance with
+ # +other_hash+ merged into current hash.
def merge!(other_hash)
@parameters.merge!(other_hash.to_h)
self
end
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
+ # current hash merged into +other_hash+.
+ def reverse_merge(other_hash)
+ new_instance_with_inherited_permitted_status(
+ other_hash.to_h.merge(@parameters)
+ )
+ end
+
+ # Returns current <tt>ActionController::Parameters</tt> instance with
+ # current hash merged into +other_hash+.
+ def reverse_merge!(other_hash)
+ @parameters.merge!(other_hash.to_h) { |key, left, right| left }
+ self
+ end
+
# This is required by ActiveModel attribute assignment, so that user can
# pass +Parameters+ to a mass assignment methods in a model. It should not
# matter as we are using +HashWithIndifferentAccess+ internally.
@@ -630,7 +715,7 @@ module ActionController
undef_method :to_param
- # Returns duplicate of object including all parameters
+ # Returns duplicate of object including all parameters.
def deep_dup
self.class.new(@parameters.deep_dup).tap do |duplicate|
duplicate.permitted = @permitted
@@ -818,7 +903,6 @@ module ActionController
# Filter this one out.
end
end
- sanitized.permitted = true
end
end
@@ -851,7 +935,7 @@ module ActionController
# whitelisted.
#
# In addition, parameters can be marked as required and flow through a
- # predefined raise/rescue flow to end up as a 400 Bad Request with no
+ # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no
# effort.
#
# class PeopleController < ActionController::Base
@@ -864,7 +948,7 @@ module ActionController
# end
#
# # This will pass with flying colors as long as there's a person key in the
- # # parameters, otherwise it'll raise an ActionController::MissingParameter
+ # # parameters, otherwise it'll raise an ActionController::ParameterMissing
# # exception, which will get caught by ActionController::Base and turned
# # into a 400 Bad Request reply.
# def update
@@ -875,7 +959,7 @@ module ActionController
#
# private
# # Using a private method to encapsulate the permissible parameters is
- # # just a good pattern since you'll be able to reuse the same permit
+ # # a good pattern since you'll be able to reuse the same permit
# # list between create and update. Also, you can specialize this method
# # with per-user checking of permissible attributes.
# def person_params
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 9f3cc099d6..21ed5b4ec8 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -3,7 +3,7 @@ module ActionController
# the <tt>_routes</tt> method. Otherwise, an exception will be raised.
#
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
- # url options like the +host+. In order to do so, this module requires the host class
+ # URL options like the +host+. In order to do so, this module requires the host class
# to implement +env+ which needs to be Rack-compatible and +request+
# which is either an instance of +ActionDispatch::Request+ or an object
# that responds to the +host+, +optional_port+, +protocol+ and
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index a7cdfe6a98..fadfc8de60 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -42,7 +42,7 @@ module ActionController
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
- # Ensure readers methods get compiled
+ # Ensure readers methods get compiled.
options.asset_host ||= app.config.asset_host
options.relative_url_root ||= app.config.relative_url_root
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
index 3ff80e6a39..cbb719d8b2 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -5,7 +5,7 @@ module ActionController
# without requirement of being in controller actions.
#
# You get a concrete renderer class by invoking ActionController::Base#renderer.
- # For example,
+ # For example:
#
# ApplicationController.renderer
#
@@ -18,7 +18,7 @@ module ActionController
# ApplicationController.render template: '...'
#
# #render allows you to use the same options that you can use when rendering in a controller.
- # For example,
+ # For example:
#
# FooController.render :action, locals: { ... }, assigns: { ... }
#
@@ -56,11 +56,12 @@ module ActionController
# Create a new renderer for the same controller but with new defaults.
def with_defaults(defaults)
- self.class.new controller, env, self.defaults.merge(defaults)
+ self.class.new controller, @env, self.defaults.merge(defaults)
end
# Accepts a custom Rack environment to render templates in.
- # It will be merged with ActionController::Renderer.defaults
+ # It will be merged with the default Rack environment defined by
+ # +ActionController::Renderer::DEFAULTS+.
def initialize(controller, env, defaults)
@controller = controller
@defaults = defaults
@@ -84,6 +85,7 @@ module ActionController
def normalize_keys(env)
new_env = {}
env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
+ new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
new_env
end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 85f2501d42..bc42d50205 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -3,6 +3,7 @@ require "active_support/core_ext/hash/conversions"
require "active_support/core_ext/object/to_query"
require "active_support/core_ext/module/anonymous"
require "active_support/core_ext/hash/keys"
+require "active_support/testing/constant_lookup"
require "action_controller/template_assertions"
require "rails-dom-testing"
@@ -12,10 +13,10 @@ module ActionController
end
module Live
- # Disable controller / rendering threads in tests. User tests can access
+ # Disable controller / rendering threads in tests. User tests can access
# the database on the main thread, so they could open a txn, then the
# controller thread will open a new connection and try to access data
- # that's only visible to the main thread's txn. This is the problem in #23483
+ # that's only visible to the main thread's txn. This is the problem in #23483.
remove_method :new_controller_thread
def new_controller_thread # :nodoc:
yield
@@ -34,7 +35,7 @@ module ActionController
attr_reader :controller_class
- # Create a new test request with default `env` values
+ # Create a new test request with default `env` values.
def self.create(controller_class)
env = {}
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
@@ -130,7 +131,7 @@ module ActionController
include Rack::Test::Utils
def should_multipart?(params)
- # FIXME: lifted from Rack-Test. We should push this separation upstream
+ # FIXME: lifted from Rack-Test. We should push this separation upstream.
multipart = false
query = lambda { |value|
case value
@@ -299,7 +300,7 @@ module ActionController
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
- # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>.
+ # On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
@@ -353,7 +354,7 @@ module ActionController
end
def controller_class
- if current_controller_class = self._controller_class
+ if current_controller_class = _controller_class
current_controller_class
else
self.controller_class = determine_default_controller_class(name)
@@ -514,8 +515,6 @@ module ActionController
@request = @controller.request
@response = @controller.response
- @request.delete_header "HTTP_COOKIE"
-
if @request.have_cookie_jar?
unless @request.cookie_jar.committed?
@request.cookie_jar.write(@response)
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 7931e53c3e..303790e96d 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2017 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -97,6 +97,8 @@ module ActionDispatch
autoload :TestResponse
autoload :AssertionResponse
end
+
+ autoload :SystemTestCase, "action_dispatch/system_test_case"
end
autoload :Mime, "action_dispatch/http/mime_type"
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index e5874a39f6..e584b84d92 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -51,28 +51,28 @@ module ActionDispatch
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end
- protected
+ private
- def parameter_filter
+ def parameter_filter # :doc:
parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
- def env_filter
+ def env_filter # :doc:
user_key = fetch_header("action_dispatch.parameter_filter") {
return NULL_ENV_FILTER
}
parameter_filter_for(Array(user_key) + ENV_MATCH)
end
- def parameter_filter_for(filters)
+ def parameter_filter_for(filters) # :doc:
ParameterFilter.new(filters)
end
KV_RE = "[^&;=]+"
PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
- def filtered_query_string
+ def filtered_query_string # :doc:
query_string.gsub(PAIR_RE) do |_|
parameter_filter.filter([[$1, $2]]).first.join("=")
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index e5f20003a3..19f89edbc1 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -135,9 +135,7 @@ module ActionDispatch
}
end
- # Receives an array of mimes and return the first user sent mime that
- # matches the order array.
- #
+ # Returns the first MIME type that matches the provided array of MIME types.
def negotiate_mime(order)
formats.each do |priority|
if priority == Mime::ALL
@@ -150,20 +148,20 @@ module ActionDispatch
order.include?(Mime::ALL) ? format : nil
end
- protected
+ private
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
- def valid_accept_header
+ def valid_accept_header # :doc:
(xhr? && (accept.present? || content_mime_type)) ||
(accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
end
- def use_accept_header
+ def use_accept_header # :doc:
!self.class.ignore_accept_header
end
- def format_from_path_extension
+ def format_from_path_extension # :doc:
path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
if match = path && path.match(/\.(\w+)\z/)
Mime[match.captures.first]
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 58eb8d0baf..0cf010f59e 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,7 +1,6 @@
# -*- frozen-string-literal: true -*-
require "singleton"
-require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/string/starts_ends_with"
module Mime
@@ -47,7 +46,7 @@ module Mime
end
end
- # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
+ # Encapsulates the notion of a MIME type. Can be used at render time, for example, with:
#
# class PostsController < ActionController::Base
# def show
@@ -65,7 +64,7 @@ module Mime
@register_callbacks = []
- # A simple helper class used in parsing the accept header
+ # A simple helper class used in parsing the accept header.
class AcceptItem #:nodoc:
attr_accessor :index, :name, :q
alias :to_s :name
@@ -73,7 +72,7 @@ module Mime
def initialize(index, name, q = nil)
@index = index
@name = name
- q ||= 0.0 if @name == "*/*".freeze # default wildcard match to end of list
+ q ||= 0.0 if @name == "*/*".freeze # Default wildcard match to end of list.
@q = ((q || 1.0).to_f * 100).to_i
end
@@ -91,22 +90,22 @@ module Mime
text_xml_idx = find_item_by_name list, "text/xml"
app_xml_idx = find_item_by_name list, Mime[:xml].to_s
- # Take care of the broken text/xml entry by renaming or deleting it
+ # Take care of the broken text/xml entry by renaming or deleting it.
if text_xml_idx && app_xml_idx
app_xml = list[app_xml_idx]
text_xml = list[text_xml_idx]
- app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two
- if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
+ app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two.
+ if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list.
list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
end
- list.delete_at(text_xml_idx) # delete text_xml from the list
+ list.delete_at(text_xml_idx) # Delete text_xml from the list.
elsif text_xml_idx
list[text_xml_idx].name = Mime[:xml].to_s
end
- # Look for more specific XML-based types and sort them ahead of app/xml
+ # Look for more specific XML-based types and sort them ahead of app/xml.
if app_xml_idx
app_xml = list[app_xml_idx]
idx = app_xml_idx
@@ -148,7 +147,7 @@ module Mime
EXTENSION_LOOKUP[extension.to_s]
end
- # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
+ # Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for
# rendering different HTML versions depending on the user agent, like an iPhone.
def register_alias(string, symbol, extension_synonyms = [])
register(string, symbol, [], extension_synonyms, true)
@@ -278,6 +277,8 @@ 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
@@ -295,8 +296,8 @@ module Mime
end
end
- def respond_to_missing?(method, include_private = false) #:nodoc:
- method.to_s.ends_with? "?"
+ def respond_to_missing?(method, include_private = false)
+ (method.to_s.ends_with? "?") || super
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index ddd15b748b..79a2ef965b 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -13,7 +13,7 @@ module ActionDispatch
}
# Raised when raw data from the request cannot be parsed by the parser
- # defined for request's content mime type.
+ # defined for request's content MIME type.
class ParseError < StandardError
def initialize
super($!.message)
@@ -22,6 +22,7 @@ module ActionDispatch
included do
class << self
+ # Returns the parameter parsers.
attr_reader :parameter_parsers
end
@@ -29,7 +30,16 @@ module ActionDispatch
end
module ClassMethods
- def parameter_parsers=(parsers) # :nodoc:
+ # Configure the parameter parser for a given MIME type.
+ #
+ # It accepts a hash where the key is the symbol of the MIME type
+ # and the value is a proc.
+ #
+ # original_parsers = ActionDispatch::Request.parameter_parsers
+ # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} }
+ # new_parsers = original_parsers.merge(xml: xml_parser)
+ # ActionDispatch::Request.parameter_parsers = new_parsers
+ def parameter_parsers=(parsers)
@parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
end
end
@@ -45,7 +55,7 @@ module ActionDispatch
query_parameters.dup
end
params.merge!(path_parameters)
- params = set_custom_encoding(params)
+ params = set_binary_encoding(params)
set_header("action_dispatch.request.parameters", params)
params
end
@@ -73,21 +83,16 @@ module ActionDispatch
private
- def set_custom_encoding(params)
+ def set_binary_encoding(params)
action = params[:action]
- params.each do |k, v|
- if v.is_a?(String) && v.encoding != encoding_template(action, k)
- params[k] = v.force_encoding(encoding_template(action, k))
+ if controller_class.binary_params_for?(action)
+ ActionDispatch::Request::Utils.each_param_value(params) do |param|
+ param.force_encoding ::Encoding::ASCII_8BIT
end
end
-
params
end
- def encoding_template(action, param)
- controller_class.encoding_for_param(action, param)
- end
-
def parse_formatted_parameters(parsers)
return yield if content_length.zero? || content_mime_type.nil?
@@ -95,7 +100,7 @@ module ActionDispatch
begin
strategy.call(raw_post)
- rescue # JSON or Ruby code block errors
+ rescue # JSON or Ruby code block errors.
my_logger = logger || ActiveSupport::Logger.new($stderr)
my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}"
@@ -110,6 +115,7 @@ module ActionDispatch
end
module ParamsParser
- ParseError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("ActionDispatch::ParamsParser::ParseError", "ActionDispatch::Http::Parameters::ParseError")
+ 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 9986d6e1e9..6d42404a98 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -69,7 +69,7 @@ module ActionDispatch
PASS_NOT_FOUND = Class.new { # :nodoc:
def self.action(_); self; end
def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
- def self.encoding_for_param(action, param); ::Encoding::UTF_8; end
+ def self.binary_params_for?(action); false; end
}
def controller_class
@@ -85,6 +85,9 @@ module ActionDispatch
end
end
+ # Returns true if the request has a header matching the given key parameter.
+ #
+ # request.key? :ip_spoofing_check # => true
def key?(key)
has_header? key
end
@@ -111,7 +114,7 @@ module ActionDispatch
HTTP_METHOD_LOOKUP = {}
- # Populate the HTTP method lookup cache
+ # Populate the HTTP method lookup cache.
HTTP_METHODS.each { |method|
HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
}
@@ -162,12 +165,12 @@ module ActionDispatch
def show_exceptions? # :nodoc:
# We're treating `nil` as "unset", and we want the default setting to be
- # `true`. This logic should be extracted to `env_config` and calculated
+ # `true`. This logic should be extracted to `env_config` and calculated
# once.
!(get_header("action_dispatch.show_exceptions".freeze) == false)
end
- # Returns a symbol form of the #request_method
+ # Returns a symbol form of the #request_method.
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
end
@@ -179,7 +182,7 @@ module ActionDispatch
@method ||= check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD"))
end
- # Returns a symbol form of the #method
+ # Returns a symbol form of the #method.
def method_symbol
HTTP_METHOD_LOOKUP[method]
end
@@ -264,7 +267,7 @@ module ActionDispatch
# (which sets the action_dispatch.request_id environment variable).
#
# This unique ID is useful for tracing a request from end-to-end as part of logging or debugging.
- # This relies on the rack variable set by the ActionDispatch::RequestId middleware.
+ # This relies on the Rack variable set by the ActionDispatch::RequestId middleware.
def request_id
get_header ACTION_DISPATCH_REQUEST_ID
end
@@ -336,7 +339,7 @@ module ActionDispatch
Session::Options.set self, options
end
- # Override Rack's GET method to support indifferent access
+ # Override Rack's GET method to support indifferent access.
def GET
fetch_header("action_dispatch.request.query_parameters") do |k|
rack_query_params = super || {}
@@ -349,7 +352,7 @@ module ActionDispatch
end
alias :query_parameters :GET
- # Override Rack's POST method to support indifferent access
+ # Override Rack's POST method to support indifferent access.
def POST
fetch_header("action_dispatch.request.request_parameters") do
pr = parse_formatted_parameters(params_parsers) do |params|
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 357ca56036..2ee52eeb48 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -85,7 +85,7 @@ module ActionDispatch # :nodoc:
cattr_accessor(:default_headers)
include Rack::Response::Helpers
- # Aliasing these off because AD::Http::Cache::Response defines them
+ # Aliasing these off because AD::Http::Cache::Response defines them.
alias :_cache_control :cache_control
alias :_cache_control= :cache_control=
@@ -142,7 +142,7 @@ module ActionDispatch # :nodoc:
private
def each_chunk(&block)
- @buf.each(&block) # extract into own method
+ @buf.each(&block)
end
end
@@ -227,7 +227,9 @@ module ActionDispatch # :nodoc:
return unless content_type
new_header_info = parse_content_type(content_type.to_s)
prev_header_info = parsed_content_type_header
- set_content_type new_header_info.mime_type, new_header_info.charset || prev_header_info.charset || self.class.default_charset
+ charset = new_header_info.charset || prev_header_info.charset
+ charset ||= self.class.default_charset unless prev_header_info.mime_type
+ set_content_type new_header_info.mime_type, charset
end
# Sets the HTTP response's content MIME type. For example, in the controller
@@ -408,7 +410,7 @@ module ActionDispatch # :nodoc:
def parse_content_type(content_type)
if content_type
type, charset = content_type.split(/;\s*charset=/)
- type = nil if type.empty?
+ type = nil if type && type.empty?
ContentTypeHeader.new(type, charset)
else
NullContentTypeHeader
@@ -423,7 +425,7 @@ module ActionDispatch # :nodoc:
def set_content_type(content_type, charset)
type = (content_type || "").dup
- type << "; charset=#{charset}" if charset
+ type << "; charset=#{charset.to_s.downcase}" if charset
set_header CONTENT_TYPE, type
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index a6937d54ff..b6be48a48b 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -101,10 +101,8 @@ module ActionDispatch
end
def add_trailing_slash(path)
- # includes querysting
if path.include?("?")
path.sub!(/\?/, '/\&')
- # does not have a .format
elsif !path.include?(".")
path.sub!(/[^\/]\z|\A\z/, '\&/')
end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 20ff4441a0..326f4e52f9 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -15,7 +15,7 @@ module ActionDispatch
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
- missing_keys = nil # need for variable scope
+ missing_keys = nil
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -36,7 +36,7 @@ module ActionDispatch
route.parts.reverse_each do |key|
break if defaults[key].nil? && parameterized_parts[key].present?
- break if parameterized_parts[key].to_s != defaults[key].to_s
+ next if parameterized_parts[key].to_s != defaults[key].to_s
break if required_parts.include?(key)
parameterized_parts.delete(key)
@@ -92,7 +92,11 @@ module ActionDispatch
else
routes = non_recursive(cache, options)
- hash = routes.group_by { |_, r| r.score(options) }
+ supplied_keys = options.each_with_object({}) do |(k, v), h|
+ h[k.to_s] = true if v
+ end
+
+ hash = routes.group_by { |_, r| r.score(supplied_keys) }
hash.keys.sort.reverse_each do |score|
break if score < 0
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index beb9f1ef3b..e1ac2c873e 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -109,7 +109,6 @@ module ActionDispatch
svg = to_svg
javascripts = [states, fsm_js]
- # Annoying hack warnings
fun_routes = fun_routes
stylesheets = stylesheets
svg = svg
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index ee91b11b42..e002755bcf 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -1,198 +1,199 @@
#
# DO NOT MODIFY!!!!
-# This file is automatically generated by Racc 1.4.11
-# from Racc grammer file "".
+# This file is automatically generated by Racc 1.4.14
+# from Racc grammar file "".
#
-require "racc/parser.rb"
+require 'racc/parser.rb'
+
+# :stopdoc:
require "action_dispatch/journey/parser_extras"
module ActionDispatch
- # :stopdoc:
module Journey
class Parser < Racc::Parser
- ##### State transition tables begin ###
-
- racc_action_table = [
- 13, 15, 14, 7, 21, 16, 8, 19, 13, 15,
- 14, 7, 17, 16, 8, 13, 15, 14, 7, 24,
- 16, 8, 13, 15, 14, 7, 19, 16, 8 ]
-
- racc_action_check = [
- 2, 2, 2, 2, 17, 2, 2, 2, 0, 0,
- 0, 0, 1, 0, 0, 19, 19, 19, 19, 20,
- 19, 19, 7, 7, 7, 7, 22, 7, 7 ]
-
- racc_action_pointer = [
- 6, 12, -2, nil, nil, nil, nil, 20, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, 4, nil, 13,
- 13, nil, 17, nil, nil ]
-
- racc_action_default = [
- -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
- -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
- -19, 25, -8, -9, -7 ]
-
- racc_goto_table = [
- 1, 22, 18, 23, nil, nil, nil, 20 ]
-
- racc_goto_check = [
- 1, 2, 1, 3, nil, nil, nil, 1 ]
-
- racc_goto_pointer = [
- nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
- nil ]
-
- racc_goto_default = [
- nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
- 12 ]
-
- racc_reduce_table = [
- 0, 0, :racc_error,
- 2, 11, :_reduce_1,
- 1, 11, :_reduce_2,
- 1, 11, :_reduce_none,
- 1, 12, :_reduce_none,
- 1, 12, :_reduce_none,
- 1, 12, :_reduce_none,
- 3, 15, :_reduce_7,
- 3, 13, :_reduce_8,
- 3, 13, :_reduce_9,
- 1, 16, :_reduce_10,
- 1, 14, :_reduce_none,
- 1, 14, :_reduce_none,
- 1, 14, :_reduce_none,
- 1, 14, :_reduce_none,
- 1, 19, :_reduce_15,
- 1, 17, :_reduce_16,
- 1, 18, :_reduce_17,
- 1, 20, :_reduce_18 ]
-
- racc_reduce_n = 19
-
- racc_shift_n = 25
-
- racc_token_table = {
- false => 0,
- :error => 1,
- :SLASH => 2,
- :LITERAL => 3,
- :SYMBOL => 4,
- :LPAREN => 5,
- :RPAREN => 6,
- :DOT => 7,
- :STAR => 8,
- :OR => 9 }
-
- racc_nt_base = 10
-
- racc_use_result_var = false
-
- Racc_arg = [
- racc_action_table,
- racc_action_check,
- racc_action_default,
- racc_action_pointer,
- racc_goto_table,
- racc_goto_check,
- racc_goto_default,
- racc_goto_pointer,
- racc_nt_base,
- racc_reduce_table,
- racc_token_table,
- racc_shift_n,
- racc_reduce_n,
- racc_use_result_var ]
-
- Racc_token_to_s_table = [
- "$end",
- "error",
- "SLASH",
- "LITERAL",
- "SYMBOL",
- "LPAREN",
- "RPAREN",
- "DOT",
- "STAR",
- "OR",
- "$start",
- "expressions",
- "expression",
- "or",
- "terminal",
- "group",
- "star",
- "symbol",
- "literal",
- "slash",
- "dot" ]
-
- Racc_debug_parser = false
-
- ##### State transition tables end #####
-
- # reduce 0 omitted
-
- def _reduce_1(val, _values)
- Cat.new(val.first, val.last)
- end
-
- def _reduce_2(val, _values)
- val.first
- end
-
- # reduce 3 omitted
-
- # reduce 4 omitted
-
- # reduce 5 omitted
-
- # reduce 6 omitted
-
- def _reduce_7(val, _values)
- Group.new(val[1])
- end
-
- def _reduce_8(val, _values)
- Or.new([val.first, val.last])
- end
-
- def _reduce_9(val, _values)
- Or.new([val.first, val.last])
- end
-
- def _reduce_10(val, _values)
- Star.new(Symbol.new(val.last))
- end
-
- # reduce 11 omitted
-
- # reduce 12 omitted
-
- # reduce 13 omitted
-
- # reduce 14 omitted
-
- def _reduce_15(val, _values)
- Slash.new("/")
- end
-
- def _reduce_16(val, _values)
- Symbol.new(val.first)
- end
-
- def _reduce_17(val, _values)
- Literal.new(val.first)
- end
-
- def _reduce_18(val, _values)
- Dot.new(val.first)
- end
-
- def _reduce_none(val, _values)
- val[0]
- end
+##### State transition tables begin ###
+
+racc_action_table = [
+ 13, 15, 14, 7, 19, 16, 8, 19, 13, 15,
+ 14, 7, 17, 16, 8, 13, 15, 14, 7, 21,
+ 16, 8, 13, 15, 14, 7, 24, 16, 8 ]
+
+racc_action_check = [
+ 2, 2, 2, 2, 22, 2, 2, 2, 19, 19,
+ 19, 19, 1, 19, 19, 7, 7, 7, 7, 17,
+ 7, 7, 0, 0, 0, 0, 20, 0, 0 ]
+
+racc_action_pointer = [
+ 20, 12, -2, nil, nil, nil, nil, 13, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, 19, nil, 6,
+ 20, nil, -5, nil, nil ]
+
+racc_action_default = [
+ -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
+ -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
+ -19, 25, -8, -9, -7 ]
+
+racc_goto_table = [
+ 1, 22, 18, 23, nil, nil, nil, 20 ]
+
+racc_goto_check = [
+ 1, 2, 1, 3, nil, nil, nil, 1 ]
+
+racc_goto_pointer = [
+ nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
+ nil ]
+
+racc_goto_default = [
+ nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
+ 12 ]
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 2, 11, :_reduce_1,
+ 1, 11, :_reduce_2,
+ 1, 11, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 3, 15, :_reduce_7,
+ 3, 13, :_reduce_8,
+ 3, 13, :_reduce_9,
+ 1, 16, :_reduce_10,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 19, :_reduce_15,
+ 1, 17, :_reduce_16,
+ 1, 18, :_reduce_17,
+ 1, 20, :_reduce_18 ]
+
+racc_reduce_n = 19
+
+racc_shift_n = 25
+
+racc_token_table = {
+ false => 0,
+ :error => 1,
+ :SLASH => 2,
+ :LITERAL => 3,
+ :SYMBOL => 4,
+ :LPAREN => 5,
+ :RPAREN => 6,
+ :DOT => 7,
+ :STAR => 8,
+ :OR => 9 }
+
+racc_nt_base = 10
+
+racc_use_result_var = false
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+ "$end",
+ "error",
+ "SLASH",
+ "LITERAL",
+ "SYMBOL",
+ "LPAREN",
+ "RPAREN",
+ "DOT",
+ "STAR",
+ "OR",
+ "$start",
+ "expressions",
+ "expression",
+ "or",
+ "terminal",
+ "group",
+ "star",
+ "symbol",
+ "literal",
+ "slash",
+ "dot" ]
+
+Racc_debug_parser = false
+
+##### State transition tables end #####
+
+# reduce 0 omitted
+
+def _reduce_1(val, _values)
+ Cat.new(val.first, val.last)
+end
+
+def _reduce_2(val, _values)
+ val.first
+end
+
+# reduce 3 omitted
+
+# reduce 4 omitted
+
+# reduce 5 omitted
+
+# reduce 6 omitted
+
+def _reduce_7(val, _values)
+ Group.new(val[1])
+end
+
+def _reduce_8(val, _values)
+ Or.new([val.first, val.last])
+end
+
+def _reduce_9(val, _values)
+ Or.new([val.first, val.last])
+end
+
+def _reduce_10(val, _values)
+ Star.new(Symbol.new(val.last))
+end
+
+# reduce 11 omitted
+
+# reduce 12 omitted
+
+# reduce 13 omitted
+
+# reduce 14 omitted
+
+def _reduce_15(val, _values)
+ Slash.new(val.first)
+end
+
+def _reduce_16(val, _values)
+ Symbol.new(val.first)
+end
+
+def _reduce_17(val, _values)
+ Literal.new(val.first)
+end
+
+def _reduce_18(val, _values)
+ Dot.new(val.first)
+end
+
+def _reduce_none(val, _values)
+ val[0]
+end
+
end # class Parser
- end # module Journey
- # :startdoc:
-end # module ActionDispatch
+ end # module Journey
+ end # module ActionDispatch
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
index d3f7c4d765..f9b1a7a958 100644
--- a/actionpack/lib/action_dispatch/journey/parser.y
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -30,7 +30,7 @@ rule
| dot
;
slash
- : SLASH { Slash.new('/') }
+ : SLASH { Slash.new(val.first) }
;
symbol
: SYMBOL { Symbol.new(val.first) }
@@ -45,5 +45,6 @@ rule
end
---- header
+# :stopdoc:
-require 'action_dispatch/journey/parser_extras'
+require "action_dispatch/journey/parser_extras"
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 0902b9233e..cf0108ec32 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -31,6 +31,13 @@ module ActionDispatch
Visitors::FormatBuilder.new.accept(spec)
end
+ def eager_load!
+ required_names
+ offsets
+ to_regexp
+ nil
+ end
+
def ast
@spec.find_all(&:symbol?).each do |node|
re = @requirements[node.to_sym]
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 0cc8d83ac8..7bc15aa6b3 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -73,6 +73,14 @@ module ActionDispatch
@internal = internal
end
+ def eager_load!
+ path.eager_load!
+ ast
+ parts
+ required_defaults
+ nil
+ end
+
def ast
@decorated_ast ||= begin
decorated_ast = path.ast
@@ -81,8 +89,15 @@ module ActionDispatch
end
end
+ # Needed for `rails routes`. Picks up succinctly defined requirements
+ # for a route, for example route
+ #
+ # get 'photo/:id', :controller => 'photos', :action => 'show',
+ # :id => /[A-Z]\d{5}/
+ #
+ # will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/}
+ # as requirements.
def requirements
- # needed for rails `rails routes`
@defaults.merge(path.requirements).delete_if { |_, v|
/.+?/ == v
}
@@ -96,13 +111,18 @@ module ActionDispatch
required_parts + required_defaults.keys
end
- def score(constraints)
+ def score(supplied_keys)
required_keys = path.required_names
- supplied_keys = constraints.map { |k, v| v && k.to_s }.compact
- return -1 unless (required_keys - supplied_keys).empty?
+ required_keys.each do |k|
+ return -1 unless supplied_keys.include?(k)
+ end
+
+ score = 0
+ path.names.each do |k|
+ score += 1 if supplied_keys.include?(k)
+ end
- score = (supplied_keys & path.names).length
score + (required_defaults.length * 2)
end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 084ae9325e..d55e1399e4 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -22,6 +22,13 @@ module ActionDispatch
@routes = routes
end
+ def eager_load!
+ # Eagerly trigger the simulator's initialization so
+ # it doesn't happen during a request cycle.
+ simulator
+ nil
+ end
+
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index ce5d350763..ffcd875b4d 100644
--- a/actionpack/lib/action_dispatch/journey/router/utils.rb
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -5,7 +5,7 @@ module ActionDispatch
# Normalizes URI path.
#
# Strips off trailing slash and ensures there is a leading slash.
- # Also converts downcase url encoded string to uppercase.
+ # Also converts downcase URL encoded string to uppercase.
#
# normalize_path("/foo") # => "/foo"
# normalize_path("/foo/") # => "/foo"
@@ -58,12 +58,12 @@ module ActionDispatch
uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
end
- protected
- def escape(component, pattern)
+ private
+ def escape(component, pattern) # :doc:
component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
end
- def percent_encode(unsafe)
+ def percent_encode(unsafe) # :doc:
safe = EMPTY.dup
unsafe.each_byte { |b| safe << DEC2HEX[b] }
safe
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 4b8c8ab063..7dbb39b26d 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require "strscan"
module ActionDispatch
@@ -35,22 +36,23 @@ module ActionDispatch
def scan
case
# /
- when text = @ss.scan(/\//)
- [:SLASH, text]
+ when @ss.skip(/\//)
+ [:SLASH, "/"]
+ when @ss.skip(/\(/)
+ [:LPAREN, "("]
+ when @ss.skip(/\)/)
+ [:RPAREN, ")"]
+ when @ss.skip(/\|/)
+ [:OR, "|"]
+ when @ss.skip(/\./)
+ [:DOT, "."]
+ when text = @ss.scan(/:\w+/)
+ [:SYMBOL, text]
when text = @ss.scan(/\*\w+/)
[:STAR, text]
- when text = @ss.scan(/(?<!\\)\(/)
- [:LPAREN, text]
- when text = @ss.scan(/(?<!\\)\)/)
- [:RPAREN, text]
- when text = @ss.scan(/\|/)
- [:OR, text]
- when text = @ss.scan(/\./)
- [:DOT, text]
- when text = @ss.scan(/(?<!\\):\w+/)
- [:SYMBOL, text]
- when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/)
- [:LITERAL, text.tr('\\', "")]
+ when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
+ text.tr! "\\", ""
+ [:LITERAL, text]
# any char
when text = @ss.scan(/./)
[:LITERAL, text]
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index cda859cba4..335797f4b9 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -5,7 +5,7 @@ module ActionDispatch
ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
- class Parameter < Struct.new(:name, :escaper)
+ Parameter = Struct.new(:name, :escaper) do
def escape(value); escaper.call value; end
end
@@ -154,7 +154,7 @@ module ActionDispatch
end
end
- # Loop through the requirements AST
+ # Loop through the requirements AST.
class Each < FunctionalVisitor # :nodoc:
def visit(node, block)
block.call(node)
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index fef246532b..ff129cf96a 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,4 +1,3 @@
-
module ActionDispatch
# Provides callbacks to be executed before and after dispatching the request.
class Callbacks
@@ -7,17 +6,6 @@ module ActionDispatch
define_callbacks :call
class << self
- def to_prepare(*args, &block)
- ActiveSupport::Reloader.to_prepare(*args, &block)
- end
-
- def to_cleanup(*args, &block)
- ActiveSupport::Reloader.to_complete(*args, &block)
- end
-
- deprecate to_prepare: "use ActiveSupport::Reloader.to_prepare instead",
- to_cleanup: "use ActiveSupport::Reloader.to_complete instead"
-
def before(*args, &block)
set_callback(:call, :before, *args, &block)
end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 956c53e813..e565c03a8a 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -160,7 +160,7 @@ module ActionDispatch
# Raised when storing more than 4K of session data.
CookieOverflow = Class.new StandardError
- # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
module ChainedCookieJars
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
#
@@ -345,16 +345,16 @@ module ActionDispatch
options[:path] ||= "/"
if options[:domain] == :all || options[:domain] == "all"
- # if there is a provided tld length then we use it otherwise default domain regexp
+ # If there is a provided tld length then we use it otherwise default domain regexp.
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
- # if host is not ip and matches domain regexp
+ # If host is not ip and matches domain regexp.
# (ip confirms to domain regexp so we explicitly check for ip)
options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
".#{$&}"
end
elsif options[:domain].is_a? Array
- # if host matches one of the supplied domains without a dot in front of it
+ # If host matches one of the supplied domains without a dot in front of it.
options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
end
end
@@ -404,7 +404,7 @@ module ActionDispatch
@delete_cookies[name.to_s] == options
end
- # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
def clear(options = {})
@cookies.each_key { |k| delete(k, options) }
end
@@ -572,7 +572,7 @@ module ActionDispatch
super
if ActiveSupport::LegacyKeyGenerator === key_generator
- raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
+ raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " \
"Read the upgrade documentation to learn more about this new config option."
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 6dddcc6ee1..6b29ce63ba 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -65,7 +65,7 @@ module ActionDispatch
self.flash = flash_hash.dup
end
- if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
+ if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
session.key?("flash") && session["flash"].nil?
session.delete("flash")
end
@@ -281,7 +281,8 @@ module ActionDispatch
@now
end
- def stringify_array(array)
+ private
+ def stringify_array(array) # :doc:
array.map do |item|
item.kind_of?(Symbol) ? item.to_s : item
end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 90c64037aa..6d64b1424b 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,54 +1,10 @@
module ActionDispatch
- # ActionDispatch::Reloader provides prepare and cleanup callbacks,
- # intended to assist with code reloading during development.
- #
- # Prepare callbacks are run before each request, and cleanup callbacks
- # after each request. In this respect they are analogs of ActionDispatch::Callback's
- # before and after callbacks. However, cleanup callbacks are not called until the
- # request is fully complete -- that is, after #close has been called on
- # the response body. This is important for streaming responses such as the
- # following:
- #
- # self.response_body = -> (response, output) do
- # # code here which refers to application models
- # end
- #
- # Cleanup callbacks will not be called until after the response_body lambda
- # is evaluated, ensuring that it can refer to application models and other
- # classes before they are unloaded.
+ # ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader
+ # callbacks, intended to assist with code reloading during development.
#
# By default, ActionDispatch::Reloader is included in the middleware stack
# only in the development environment; specifically, when +config.cache_classes+
- # is false. Callbacks may be registered even when it is not included in the
- # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
- # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
- #
+ # is false.
class Reloader < Executor
- def self.to_prepare(*args, &block)
- ActiveSupport::Reloader.to_prepare(*args, &block)
- end
-
- def self.to_cleanup(*args, &block)
- ActiveSupport::Reloader.to_complete(*args, &block)
- end
-
- def self.prepare!
- default_reloader.prepare!
- end
-
- def self.cleanup!
- default_reloader.reload!
- end
-
- class << self
- attr_accessor :default_reloader # :nodoc:
-
- deprecate to_prepare: "use ActiveSupport::Reloader.to_prepare instead",
- to_cleanup: "use ActiveSupport::Reloader.to_complete instead",
- prepare!: "use Rails.application.reloader.prepare! instead",
- cleanup!: "use Rails.application.reloader.reload! instead of cleanup + prepare"
- end
-
- self.default_reloader = ActiveSupport::Reloader
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 523eeb5b05..53d5a4918c 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -131,8 +131,8 @@ module ActionDispatch
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
if should_check_ip && !forwarded_ips.include?(client_ips.last)
# We don't know which came from the proxy, and which from the user
- raise IpSpoofAttackError, "IP spoofing attack?! " +
- "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " +
+ raise IpSpoofAttackError, "IP spoofing attack?! " \
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
end
@@ -153,17 +153,17 @@ module ActionDispatch
@ip ||= calculate_ip
end
- protected
+ private
- def ips_from(header)
+ def ips_from(header) # :doc:
return [] unless header
- # Split the comma-separated list into an array of strings
+ # Split the comma-separated list into an array of strings.
ips = header.strip.split(/[,\s]+/)
ips.select do |ip|
begin
- # Only return IPs that are valid according to the IPAddr#new method
+ # Only return IPs that are valid according to the IPAddr#new method.
range = IPAddr.new(ip).to_range
- # we want to make sure nobody is sneaking a netmask in
+ # We want to make sure nobody is sneaking a netmask in.
range.begin == range.end
rescue ArgumentError
nil
@@ -171,7 +171,7 @@ module ActionDispatch
end
end
- def filter_proxies(ips)
+ def filter_proxies(ips) # :doc:
ips.reject do |ip|
@proxies.any? { |proxy| proxy === ip }
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 49b82e7128..21ccf5a097 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -8,8 +8,8 @@ module ActionDispatch
module Session
class SessionRestoreError < StandardError #:nodoc:
def initialize
- super("Session contains objects whose class definition isn't available.\n" +
- "Remember to require the classes for all objects kept in the session.\n" +
+ super("Session contains objects whose class definition isn't available.\n" \
+ "Remember to require the classes for all objects kept in the session.\n" \
"(Original exception: #{$!.message} [#{$!.class}])\n")
set_backtrace $!.backtrace
end
@@ -27,17 +27,16 @@ module ActionDispatch
sid
end
- protected
+ private
- def initialize_sid
+ def initialize_sid # :doc:
@default_options.delete(:sidbits)
@default_options.delete(:secure_random)
end
- private
- def make_request(env)
- ActionDispatch::Request.new env
- end
+ def make_request(env)
+ ActionDispatch::Request.new env
+ end
end
module StaleSessionCheck
@@ -54,7 +53,7 @@ module ActionDispatch
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
- # Note that the regexp does not allow $1 to end with a ':'
+ # Note that the regexp does not allow $1 to end with a ':'.
$1.constantize
rescue LoadError, NameError
raise ActionDispatch::Session::SessionRestoreError
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 90f26a1c33..5a99714ec2 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -8,7 +8,7 @@ module ActionDispatch
# The exceptions app should be passed as parameter on initialization
# of ShowExceptions. Every time there is an exception, ShowExceptions will
# store the exception in env["action_dispatch.exception"], rewrite the
- # PATH_INFO to the exception status code and call the rack app.
+ # PATH_INFO to the exception status code and call the Rack app.
#
# If the application returns a "X-Cascade" pass response, this middleware
# will send an empty response as result with the correct status code.
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 5c71f0fc48..5d10129d21 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -33,7 +33,7 @@ module ActionDispatch
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
if match = paths.detect { |p|
- path = File.join(@root, p.force_encoding("UTF-8".freeze))
+ path = File.join(@root, p.force_encoding(Encoding::UTF_8))
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
index 429ea7057c..2d21ae63f5 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -60,7 +60,7 @@
<%= link_to "Path", "#", 'data-route-helper' => '_path',
title: "Returns a relative path (without the http or domain)" %> /
<%= link_to "Url", "#", 'data-route-helper' => '_url',
- title: "Returns an absolute url (with the http and domain)" %>
+ title: "Returns an absolute URL (with the http and domain)" %>
</th>
<th><%# HTTP Verb %>
</th>
@@ -93,7 +93,7 @@
}
}
- // get JSON from url and invoke callback with result
+ // get JSON from URL and invoke callback with result
function getJSON(url, success) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 48cc91bbfa..16a18a7f25 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -39,8 +39,6 @@ module ActionDispatch
config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
- ActionDispatch::Reloader.default_reloader = app.reloader
-
ActionDispatch.test_app = app
end
end
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index a2a80f39fc..74ba6466cf 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -7,10 +7,10 @@ module ActionDispatch
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
- # Singleton object used to determine if an optional param wasn't specified
+ # Singleton object used to determine if an optional param wasn't specified.
Unspecified = Object.new
- # Creates a session hash, merging the properties of the previous session if any
+ # Creates a session hash, merging the properties of the previous session if any.
def self.create(store, req, default_options)
session_was = find req
session = Request::Session.new(store, req)
@@ -63,7 +63,7 @@ module ActionDispatch
@req = req
@delegate = {}
@loaded = false
- @exists = nil # we haven't checked yet
+ @exists = nil # We haven't checked yet.
end
def id
@@ -79,7 +79,7 @@ module ActionDispatch
options = self.options || {}
@by.send(:delete_session, @req, options.id(@req), options)
- # Load the new sid to be written with the response
+ # Load the new sid to be written with the response.
@loaded = false
load_for_write!
end
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 282bdbd2be..3615e4b1d8 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -4,6 +4,17 @@ module ActionDispatch
mattr_accessor :perform_deep_munge
self.perform_deep_munge = true
+ def self.each_param_value(params, &block)
+ case params
+ when Array
+ params.each { |element| each_param_value(element, &block) }
+ when Hash
+ params.each_value { |value| each_param_value(value, &block) }
+ when String
+ block.call params
+ end
+ end
+
def self.normalize_encode_params(params)
if perform_deep_munge
NoNilParamEncoder.normalize_encode_params params
@@ -29,7 +40,6 @@ module ActionDispatch
class ParamEncoder # :nodoc:
# Convert nested Hash to HashWithIndifferentAccess.
- #
def self.normalize_encode_params(params)
case params
when Array
@@ -52,7 +62,7 @@ module ActionDispatch
end
end
- # Remove nils from the params hash
+ # Remove nils from the params hash.
class NoNilParamEncoder < ParamEncoder # :nodoc:
def self.handle_array(params)
list = super
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index fe8bf6a35c..60d4789a63 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/string/filters'
+require "active_support/core_ext/string/filters"
module ActionDispatch
# The routing module provides URL rewriting in native Ruby. It's a way to
@@ -120,7 +120,7 @@ module ActionDispatch
# controller :blog do
# get 'blog/show' => :list
# get 'blog/delete' => :delete
- # get 'blog/edit' => :edit
+ # get 'blog/edit' => :edit
# end
#
# # provides named routes for show, delete, and edit
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index b91ffb8419..9aa4b92df2 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -196,7 +196,7 @@ module ActionDispatch
@buffer << @view.render(partial: "routes/route", collection: routes)
end
- # the header is part of the HTML page, so we don't construct it here.
+ # The header is part of the HTML page, so we don't construct it here.
def header(routes)
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 4efde09b8b..8ad17504ae 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,6 +1,7 @@
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 "action_dispatch/routing/redirection"
require "action_dispatch/routing/endpoint"
@@ -16,9 +17,9 @@ module ActionDispatch
CALL = ->(app, req) { app.call req.env }
def initialize(app, constraints, strategy)
- # Unwrap Constraints objects. I don't actually think it's possible
+ # Unwrap Constraints objects. I don't actually think it's possible
# to pass a Constraints object to this constructor, but there were
- # multiple places that kept testing children of this object. I
+ # multiple places that kept testing children of this object. I
# *think* they were just being defensive, but I have no idea.
if app.is_a?(self.class)
constraints += app.constraints
@@ -217,7 +218,7 @@ module ActionDispatch
private
def add_wildcard_options(options, formatted, path_ast)
# Add a constraint for wildcard route to make it non-greedy and match the
- # optional format part of the route by default
+ # optional format part of the route by default.
if formatted != false
path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
hash[node.name.to_sym] ||= /.+?/
@@ -238,7 +239,7 @@ module ActionDispatch
options[:controller] ||= /.+?/
end
- if to.respond_to? :call
+ if to.respond_to?(:action) || to.respond_to?(:call)
options
else
to_endpoint = split_to to
@@ -290,16 +291,14 @@ module ActionDispatch
end
def app(blocks)
- if to.is_a?(Class) && to < ActionController::Metal
+ if to.respond_to?(:action)
Routing::RouteSet::StaticDispatcher.new to
+ elsif to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
else
- if to.respond_to?(:call)
- Constraints.new(to, blocks, Constraints::CALL)
- elsif blocks.any?
- Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
- else
- dispatcher(defaults.key?(:controller))
- end
+ dispatcher(defaults.key?(:controller))
end
end
@@ -397,7 +396,7 @@ module ActionDispatch
end
module Base
- # Matches a url pattern to one or more routes.
+ # Matches a URL pattern to one or more routes.
#
# You should not use the +match+ method in your router
# without specifying an HTTP method.
@@ -407,7 +406,7 @@ module ActionDispatch
# # sets :controller, :action and :id in params
# match ':controller/:action/:id', via: [:get, :post]
#
- # Note that +:controller+, +:action+ and +:id+ are interpreted as url
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
# query parameters and thus available through +params+ in an action.
#
# If you want to expose your action to GET, use +get+ in the router:
@@ -456,7 +455,7 @@ module ActionDispatch
#
# === Options
#
- # Any options not seen here are passed on as params with the url.
+ # Any options not seen here are passed on as params with the URL.
#
# [:controller]
# The route's controller.
@@ -661,7 +660,7 @@ module ActionDispatch
else
prefix_options = options.slice(*_route.segment_keys)
prefix_options[:relative_url_root] = "".freeze
- # we must actually delete prefix segment keys to avoid passing them to next url_for
+ # 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)
end
@@ -996,65 +995,65 @@ module ActionDispatch
end
private
- def merge_path_scope(parent, child) #:nodoc:
+ def merge_path_scope(parent, child)
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_shallow_path_scope(parent, child) #:nodoc:
+ def merge_shallow_path_scope(parent, child)
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_as_scope(parent, child) #:nodoc:
+ def merge_as_scope(parent, child)
parent ? "#{parent}_#{child}" : child
end
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
+ def merge_shallow_prefix_scope(parent, child)
parent ? "#{parent}_#{child}" : child
end
- def merge_module_scope(parent, child) #:nodoc:
+ def merge_module_scope(parent, child)
parent ? "#{parent}/#{child}" : child
end
- def merge_controller_scope(parent, child) #:nodoc:
+ def merge_controller_scope(parent, child)
child
end
- def merge_action_scope(parent, child) #:nodoc:
+ def merge_action_scope(parent, child)
child
end
- def merge_via_scope(parent, child) #:nodoc:
+ def merge_via_scope(parent, child)
child
end
- def merge_format_scope(parent, child) #:nodoc:
+ def merge_format_scope(parent, child)
child
end
- def merge_path_names_scope(parent, child) #:nodoc:
+ def merge_path_names_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_constraints_scope(parent, child) #:nodoc:
+ def merge_constraints_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_defaults_scope(parent, child) #:nodoc:
+ def merge_defaults_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_blocks_scope(parent, child) #:nodoc:
+ def merge_blocks_scope(parent, child)
merged = parent ? parent.dup : []
merged << child if child
merged
end
- def merge_options_scope(parent, child) #:nodoc:
+ def merge_options_scope(parent, child)
(parent || {}).merge(child)
end
- def merge_shallow_scope(parent, child) #:nodoc:
+ def merge_shallow_scope(parent, child)
child ? true : false
end
@@ -1239,7 +1238,7 @@ module ActionDispatch
#
# resource :profile
#
- # creates six different routes in your application, all mapping to
+ # This creates six different routes in your application, all mapping to
# the +Profiles+ controller (note that the controller is named after
# the plural):
#
@@ -1324,14 +1323,14 @@ module ActionDispatch
#
# resources :posts, path_names: { new: "brand_new" }
#
- # The above example will now change /posts/new to /posts/brand_new
+ # The above example will now change /posts/new to /posts/brand_new.
#
# [:path]
# Allows you to change the path prefix for the resource.
#
# resources :posts, path: 'postings'
#
- # The resource and all segments will now route to /postings instead of /posts
+ # The resource and all segments will now route to /postings instead of /posts.
#
# [:only]
# Only generate routes for the given actions.
@@ -1526,7 +1525,7 @@ module ActionDispatch
end
end
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
def namespace(path, options = {})
if resource_scope?
nested { super }
@@ -1546,7 +1545,7 @@ module ActionDispatch
!parent_resource.singleton? && @scope[:shallow]
end
- # Matches a url pattern to one or more routes.
+ # Matches a URL pattern to one or more routes.
# For more information, see match[rdoc-ref:Base#match].
#
# match 'path' => 'controller#action', via: patch
@@ -1619,13 +1618,13 @@ module ActionDispatch
end
end
- protected
+ private
- def parent_resource #:nodoc:
+ def parent_resource
@scope[:scope_level_resource]
end
- def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
+ def apply_common_behavior_for(method, resources, options, &block)
if resources.length > 1
resources.each { |r| send(method, r, options, &block) }
return true
@@ -1658,39 +1657,39 @@ module ActionDispatch
false
end
- def apply_action_options(options) # :nodoc:
+ def apply_action_options(options)
return options if action_options? options
options.merge scope_action_options
end
- def action_options?(options) #:nodoc:
+ def action_options?(options)
options[:only] || options[:except]
end
- def scope_action_options #:nodoc:
+ def scope_action_options
@scope[:action_options] || {}
end
- def resource_scope? #:nodoc:
+ def resource_scope?
@scope.resource_scope?
end
- def resource_method_scope? #:nodoc:
+ def resource_method_scope?
@scope.resource_method_scope?
end
- def nested_scope? #:nodoc:
+ def nested_scope?
@scope.nested?
end
- def with_scope_level(kind)
+ def with_scope_level(kind) # :doc:
@scope = @scope.new_level(kind)
yield
ensure
@scope = @scope.parent
end
- def resource_scope(resource) #:nodoc:
+ def resource_scope(resource)
@scope = @scope.new(scope_level_resource: resource)
controller(resource.resource_scope) { yield }
@@ -1698,7 +1697,7 @@ module ActionDispatch
@scope = @scope.parent
end
- def nested_options #:nodoc:
+ def nested_options
options = { as: parent_resource.member_name }
options[:constraints] = {
parent_resource.nested_param => param_constraint
@@ -1707,25 +1706,25 @@ module ActionDispatch
options
end
- def shallow_nesting_depth #:nodoc:
+ def shallow_nesting_depth
@scope.find_all { |node|
node.frame[:scope_level_resource]
}.count { |node| node.frame[:scope_level_resource].shallow? }
end
- def param_constraint? #:nodoc:
+ def param_constraint?
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
- def param_constraint #:nodoc:
+ def param_constraint
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action) #:nodoc:
+ def canonical_action?(action)
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scope #:nodoc:
+ def shallow_scope
scope = { as: @scope[:shallow_prefix],
path: @scope[:shallow_path] }
@scope = @scope.new scope
@@ -1735,7 +1734,7 @@ module ActionDispatch
@scope = @scope.parent
end
- def path_for_action(action, path) #:nodoc:
+ def path_for_action(action, path)
return "#{@scope[:path]}/#{path}" if path
if canonical_action?(action)
@@ -1745,11 +1744,11 @@ module ActionDispatch
end
end
- def action_path(name) #:nodoc:
+ def action_path(name)
@scope[:path_names][name.to_sym] || name
end
- def prefix_name_for_action(as, action) #:nodoc:
+ def prefix_name_for_action(as, action)
if as
prefix = as
elsif !canonical_action?(action)
@@ -1761,7 +1760,7 @@ module ActionDispatch
end
end
- def name_for_action(as, action) #:nodoc:
+ def name_for_action(as, action)
prefix = prefix_name_for_action(as, action)
name_prefix = @scope[:as]
@@ -1787,7 +1786,7 @@ module ActionDispatch
end
end
- def set_member_mappings_for_resource
+ def set_member_mappings_for_resource # :doc:
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
@@ -1799,12 +1798,10 @@ module ActionDispatch
end
end
- def api_only?
+ def api_only? # :doc:
@set.api_only?
end
- private
-
def path_scope(path)
@scope = @scope.new(path: merge_path_scope(@scope[:path], path))
yield
@@ -1868,7 +1865,7 @@ module ActionDispatch
path =~ %r{^/?[-\w]+/[-\w/]+$}
end
- def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
if on = options.delete(:on)
send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
else
@@ -1883,7 +1880,7 @@ module ActionDispatch
end
end
- def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
path = path_for_action(action, _path)
raise ArgumentError, "path is required" if path.blank?
@@ -1907,7 +1904,7 @@ module ActionDispatch
ast = Journey::Parser.parse path
mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
- @set.add_route(mapping, ast, as, anchor)
+ @set.add_route(mapping, as)
end
def match_root_route(options)
@@ -2006,7 +2003,7 @@ module ActionDispatch
# concerns :commentable
# end
#
- # concerns also work in any routes helper that you want to use:
+ # Concerns also work in any routes helper that you want to use:
#
# namespace :posts do
# concerns :commentable
@@ -2023,6 +2020,120 @@ module ActionDispatch
end
end
+ module CustomUrls
+ # 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:
+ #
+ # direct :homepage do
+ # "http://www.rubyonrails.org"
+ # end
+ #
+ # direct :commentable do |model|
+ # [ model, anchor: model.dom_id ]
+ # end
+ #
+ # direct :main do
+ # { controller: "pages", action: "index", subdomain: "www" }
+ # end
+ #
+ # The return value from the block passed to `direct` must be a valid set of
+ # 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
+ #
+ # 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.
+ #
+ # You can also specify default options that will be passed through to
+ # your URL helper definition, e.g:
+ #
+ # direct :browse, page: 1, size: 10 do |options|
+ # [ :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
+ # 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:
+ #
+ # Rails.application.routes.url_helpers.browse_path
+ #
+ # then it will raise a `NameError`. Because of this you need to be aware of the
+ # context in which you will use your custom URL helper when defining it.
+ #
+ # NOTE: The `direct` method can't be used inside of a scope block such as
+ # `namespace` or `scope` and will raise an error if it detects that it is.
+ def direct(name, options = {}, &block)
+ unless @scope.root?
+ raise RuntimeError, "The direct method can't be used inside a routes scope block"
+ end
+
+ @set.add_url_helper(name, options, &block)
+ end
+
+ # Define custom polymorphic mappings of models to URLs. This alters the
+ # behavior of `polymorphic_url` and consequently the behavior of
+ # `link_to` and `form_for` when passed a model instance, e.g:
+ #
+ # resource :basket
+ #
+ # resolve "Basket" do
+ # [:basket]
+ # end
+ #
+ # This will now generate "/basket" when a `Basket` instance is passed to
+ # `link_to` or `form_for` instead of the standard "/baskets/:id".
+ #
+ # NOTE: This custom behavior only applies to simple polymorphic URLs where
+ # a single model instance is passed and not more complicated forms, e.g:
+ #
+ # # config/routes.rb
+ # resource :profile
+ # namespace :admin do
+ # resources :users
+ # end
+ #
+ # resolve("User") { [:profile] }
+ #
+ # # app/views/application/_menu.html.erb
+ # link_to "Profile", @current_user
+ # link_to "Profile", [:admin, @current_user]
+ #
+ # The first `link_to` will generate "/profile" but the second will generate
+ # the standard polymorphic URL of "/admin/users/1".
+ #
+ # You can pass options to a polymorphic mapping - the arity for the block
+ # needs to be two as the instance is passed as the first argument, e.g:
+ #
+ # resolve "Basket", anchor: "items" do |basket, options|
+ # [:basket, options]
+ # end
+ #
+ # This generates the URL "/basket#items" because when the last item in an
+ # array passed to `polymorphic_url` is a hash then it's treated as options
+ # to the URL helper that gets called.
+ #
+ # NOTE: The `resolve` method can't be used inside of a scope block such as
+ # `namespace` or `scope` and will raise an error if it detects that it is.
+ def resolve(*args, &block)
+ unless @scope.root?
+ raise RuntimeError, "The resolve method can't be used inside a routes scope block"
+ end
+
+ options = args.extract_options!
+ args = args.flatten(1)
+
+ args.each do |klass|
+ @set.add_polymorphic_mapping(klass, options, &block)
+ end
+ end
+ end
+
class Scope # :nodoc:
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
@@ -2043,6 +2154,14 @@ module ActionDispatch
scope_level == :nested
end
+ def null?
+ @hash.nil? && @parent.nil?
+ end
+
+ def root?
+ @parent.null?
+ end
+
def resources?
scope_level == :resources
end
@@ -2116,6 +2235,7 @@ module ActionDispatch
include Scoping
include Concerns
include Resources
+ include CustomUrls
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 432b9bf4c1..e89ea8b21d 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -40,7 +40,7 @@ module ActionDispatch
#
# Example usage:
#
- # edit_polymorphic_path(@post) # => "/posts/1/edit"
+ # edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
#
# == Usage with mounted engines
@@ -103,6 +103,10 @@ module ActionDispatch
return polymorphic_url record, options
end
+ if mapping = polymorphic_mapping(record_or_hash_or_array)
+ return mapping.call(self, [record_or_hash_or_array, options], false)
+ end
+
opts = options.dup
action = opts.delete :action
type = opts.delete(:routing_type) || :url
@@ -123,6 +127,10 @@ module ActionDispatch
return polymorphic_path record, options
end
+ if mapping = polymorphic_mapping(record_or_hash_or_array)
+ return mapping.call(self, [record_or_hash_or_array, options], true)
+ end
+
opts = options.dup
action = opts.delete :action
type = :path
@@ -156,6 +164,14 @@ module ActionDispatch
polymorphic_path(record_or_hash, options.merge(action: action))
end
+ def polymorphic_mapping(record)
+ if record.respond_to?(:to_model)
+ _routes.polymorphic_mappings[record.to_model.model_name.name]
+ else
+ _routes.polymorphic_mappings[record.class.name]
+ end
+ end
+
class HelperMethodBuilder # :nodoc:
CACHE = { "path" => {}, "url" => {} }
@@ -255,9 +271,13 @@ module ActionDispatch
[named_route, args]
end
- def handle_model_call(target, model)
- method, args = handle_model model
- target.send(method, *args)
+ def handle_model_call(target, record)
+ if mapping = polymorphic_mapping(target, record)
+ mapping.call(target, [record], suffix == "path")
+ else
+ method, args = handle_model(record)
+ target.send(method, *args)
+ end
end
def handle_list(list)
@@ -303,6 +323,14 @@ module ActionDispatch
private
+ def polymorphic_mapping(target, record)
+ if record.respond_to?(:to_model)
+ target._routes.polymorphic_mappings[record.to_model.model_name.name]
+ else
+ target._routes.polymorphic_mappings[record.class.name]
+ end
+ end
+
def get_method_for_class(klass)
name = @key_strategy.call klass.model_name
get_method_for_string name
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 4e2318a45e..3bcb341758 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -36,6 +36,8 @@ module ActionDispatch
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
+ req.commit_flash
+
body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
@@ -137,11 +139,14 @@ module ActionDispatch
#
# get "/stories" => redirect("/posts")
#
+ # This will redirect the user, while ignoring certain parts of the request, including query string, etc.
+ # `/stories`, `/stories?foo=bar`, etc all redirect to `/posts`.
+ #
# You can also use interpolation in the supplied redirect argument:
#
# get 'docs/:article', to: redirect('/wiki/%{article}')
#
- # Note that if you return a path without a leading slash then the url is prefixed with the
+ # Note that if you return a path without a leading slash then the URL is prefixed with the
# current SCRIPT_NAME environment variable. This is typically '/' but may be different in
# a mounted engine or where the application is deployed to a subdirectory of a website.
#
@@ -160,11 +165,16 @@ module ActionDispatch
# Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
# the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
#
- # The options version of redirect allows you to supply only the parts of the url which need
+ # The options version of redirect allows you to supply only the parts of the URL which need
# to change, it also supports interpolation of the path similar to the first example.
#
# get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
# get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
+ # get '/stories', to: redirect(path: '/posts')
+ #
+ # This will redirect the user, while changing only the specified parts of the request,
+ # for example the `path` option in the last example.
+ # `/stories`, `/stories?foo=bar`, redirect to `/posts` and `/posts?foo=bar` respectively.
#
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
# common redirect routes. The call method must accept two arguments, params and request, and return
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 5853adb110..129e90037e 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -73,6 +73,7 @@ module ActionDispatch
@routes = {}
@path_helpers = Set.new
@url_helpers = Set.new
+ @custom_helpers = Set.new
@url_helpers_module = Module.new
@path_helpers_module = Module.new
end
@@ -88,16 +89,30 @@ module ActionDispatch
def clear!
@path_helpers.each do |helper|
- @path_helpers_module.send :undef_method, helper
+ @path_helpers_module.send :remove_method, helper
end
@url_helpers.each do |helper|
- @url_helpers_module.send :undef_method, helper
+ @url_helpers_module.send :remove_method, helper
+ end
+
+ @custom_helpers.each do |helper|
+ path_name = :"#{helper}_path"
+ url_name = :"#{helper}_url"
+
+ if @path_helpers_module.method_defined?(path_name)
+ @path_helpers_module.send :remove_method, path_name
+ end
+
+ if @url_helpers_module.method_defined?(url_name)
+ @url_helpers_module.send :remove_method, url_name
+ end
end
@routes.clear
@path_helpers.clear
@url_helpers.clear
+ @custom_helpers.clear
end
def add(name, route)
@@ -143,6 +158,23 @@ module ActionDispatch
routes.length
end
+ def add_url_helper(name, defaults, &block)
+ @custom_helpers << name
+ helper = CustomUrlHelper.new(name, defaults, &block)
+
+ @path_helpers_module.module_eval do
+ define_method(:"#{name}_path") do |*args|
+ helper.call(self, args, true)
+ end
+ end
+
+ @url_helpers_module.module_eval do
+ define_method(:"#{name}_url") do |*args|
+ helper.call(self, args, false)
+ end
+ end
+ end
+
class UrlHelper
def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
@@ -263,7 +295,7 @@ module ActionDispatch
end
private
- # Create a url helper allowing ordered parameters to be associated
+ # Create a URL helper allowing ordered parameters to be associated
# with corresponding dynamic segments, so you can do:
#
# foo_url(bar, baz, bang)
@@ -305,7 +337,7 @@ module ActionDispatch
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
attr_accessor :default_url_options
- attr_reader :env_key
+ attr_reader :env_key, :polymorphic_mappings
alias :routes :set
@@ -347,6 +379,13 @@ module ActionDispatch
@set = Journey::Routes.new
@router = Journey::Router.new @set
@formatter = Journey::Formatter.new self
+ @polymorphic_mappings = {}
+ end
+
+ def eager_load!
+ router.eager_load!
+ routes.each(&:eager_load!)
+ nil
end
def relative_url_root
@@ -402,6 +441,7 @@ module ActionDispatch
named_routes.clear
set.clear
formatter.clear
+ @polymorphic_mappings.clear
@prepend.each { |blk| eval_block(blk) }
end
@@ -446,17 +486,50 @@ module ActionDispatch
# Define url_for in the singleton level so one can do:
# Rails.application.routes.url_helpers.url_for(args)
- @_routes = routes
+ proxy_class = Class.new do
+ include UrlFor
+ include routes.named_routes.path_helpers_module
+ include routes.named_routes.url_helpers_module
+
+ attr_reader :_routes
+
+ def initialize(routes)
+ @_routes = routes
+ end
+
+ def optimize_routes_generation?
+ @_routes.optimize_routes_generation?
+ end
+ end
+
+ @_proxy = proxy_class.new(routes)
+
class << self
def url_for(options)
- @_routes.url_for(options)
+ @_proxy.url_for(options)
+ end
+
+ def full_url_for(options)
+ @_proxy.full_url_for(options)
+ end
+
+ def route_for(name, *args)
+ @_proxy.route_for(name, *args)
end
def optimize_routes_generation?
- @_routes.optimize_routes_generation?
+ @_proxy.optimize_routes_generation?
end
- attr_reader :_routes
+ def polymorphic_url(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_url(record_or_hash_or_array, options)
+ end
+
+ def polymorphic_path(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_path(record_or_hash_or_array, options)
+ end
+
+ def _routes; @_proxy._routes; end
def url_options; {}; end
end
@@ -500,7 +573,7 @@ module ActionDispatch
routes.empty?
end
- def add_route(mapping, path_ast, name, anchor)
+ def add_route(mapping, name)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
if name && named_routes[name]
@@ -517,20 +590,58 @@ 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.1.
+ will be removed in Rails 5.2.
MSG
end
if route.segment_keys.include?(:action)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :action segment in a route is deprecated and
- will be removed in Rails 5.1.
+ will be removed in Rails 5.2.
MSG
end
route
end
+ def add_polymorphic_mapping(klass, options, &block)
+ @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
+ end
+
+ def add_url_helper(name, options, &block)
+ named_routes.add_url_helper(name, options, &block)
+ end
+
+ class CustomUrlHelper
+ attr_reader :name, :defaults, :block
+
+ def initialize(name, defaults, &block)
+ @name = name
+ @defaults = defaults
+ @block = block
+ end
+
+ def call(t, args, only_path = false)
+ options = args.extract_options!
+ url = t.full_url_for(eval_block(t, args, options))
+
+ if only_path
+ "/" + url.partition(%r{(?<!/)/(?!/)}).last
+ else
+ url
+ end
+ end
+
+ private
+ def eval_block(t, args, options)
+ t.instance_exec(*args, merge_defaults(options), &block)
+ end
+
+ def merge_defaults(options)
+ defaults ? defaults.merge(options) : options
+ end
+ end
+
class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
@@ -745,8 +856,7 @@ module ActionDispatch
params[key] = URI.parser.unescape(value)
end
end
- old_params = req.path_parameters
- req.path_parameters = old_params.merge params
+ req.path_parameters = params
app = route.app
if app.matches?(req) && app.dispatcher?
begin
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index a1ac5a2b6c..008216cc80 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -113,10 +113,10 @@ module ActionDispatch
default_url_options
end
- # Generate a url based on the options provided, default_url_options and the
+ # Generate a URL based on the options provided, default_url_options and the
# routes defined in routes.rb. The following options are supported:
#
- # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+.
+ # * <tt>:only_path</tt> - If true, the relative URL is returned. Defaults to +false+.
# * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
# * <tt>:host</tt> - Specifies the host the link should be targeted at.
# If <tt>:only_path</tt> is false, this option must be
@@ -164,6 +164,10 @@ module ActionDispatch
# implicitly used by +url_for+ can always be overwritten like shown on the
# last +url_for+ calls.
def url_for(options = nil)
+ full_url_for(options)
+ end
+
+ def full_url_for(options = nil) # :nodoc:
case options
when nil
_routes.url_for(url_options.symbolize_keys)
@@ -192,20 +196,26 @@ module ActionDispatch
end
end
+ def route_for(name, *args) # :nodoc:
+ public_send(:"#{name}_url", *args)
+ end
+
protected
def optimize_routes_generation?
_routes.optimize_routes_generation? && default_url_options.empty?
end
- def _with_routes(routes)
+ private
+
+ def _with_routes(routes) # :doc:
old_routes, @_routes = @_routes, routes
yield
ensure
@_routes = old_routes
end
- def _routes_context
+ def _routes_context # :doc:
self
end
end
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
new file mode 100644
index 0000000000..9cc3d0757f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -0,0 +1,126 @@
+require "capybara/dsl"
+require "capybara/minitest"
+require "action_controller"
+require "action_dispatch/system_testing/driver"
+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"
+
+module ActionDispatch
+ # = System Testing
+ #
+ # System tests let you test applications in the browser. Because system
+ # tests use a real browser experience, you can test all of your JavaScript
+ # easily from your test suite.
+ #
+ # To create a system test in your application, extend your test class
+ # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a
+ # base and allow you to configure the settings through your
+ # <tt>application_system_test_case.rb</tt> file that is generated with a new
+ # application or scaffold.
+ #
+ # Here is an example system test:
+ #
+ # require 'application_system_test_case'
+ #
+ # class Users::CreateTest < ApplicationSystemTestCase
+ # test "adding a new user" do
+ # visit users_path
+ # click_on 'New User'
+ #
+ # fill_in 'Name', with: 'Arya'
+ # click_on 'Create User'
+ #
+ # assert_text 'Arya'
+ # end
+ # end
+ #
+ # When generating an application or scaffold, an +application_system_test_case.rb+
+ # file will also be generated containing the base class for system testing.
+ # This is where you can change the driver, add Capybara settings, and other
+ # configuration for your system tests.
+ #
+ # require "test_helper"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+ # end
+ #
+ # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the
+ # Selenium driver, with the Chrome browser, and a browser size of 1400x1400.
+ #
+ # Changing the driver configuration options is easy. Let's say you want to use
+ # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+
+ # file add the following:
+ #
+ # require "test_helper"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :selenium, using: :firefox
+ # end
+ #
+ # +driven_by+ has a required argument for the driver name. The keyword
+ # arguments are +:using+ for the browser and +:screen_size+ to change the
+ # size of the browser screen. These two options are not applicable for
+ # headless drivers and will be silently ignored if passed.
+ #
+ # 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 the +:using+
+ # option because the driver is headless.
+ #
+ # require "test_helper"
+ # require "capybara/poltergeist"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :poltergeist
+ # end
+ #
+ # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara
+ # and Rails, any driver that is supported by Capybara is supported by system
+ # tests as long as you include the required gems and files.
+ class SystemTestCase < IntegrationTest
+ include Capybara::DSL
+ include Capybara::Minitest::Assertions
+ include SystemTesting::TestHelpers::SetupAndTeardown
+ include SystemTesting::TestHelpers::ScreenshotHelper
+
+ def initialize(*) # :nodoc:
+ super
+ self.class.superclass.driver.use
+ end
+
+ def self.start_application # :nodoc:
+ Capybara.app = Rack::Builder.new do
+ map "/" do
+ run Rails.application
+ end
+ end
+
+ SystemTesting::Server.new.run
+ end
+
+ # System Test configuration options
+ #
+ # The default settings are Selenium, using Chrome, with a screen size
+ # of 1400x1400.
+ #
+ # Examples:
+ #
+ # driven_by :poltergeist
+ #
+ # driven_by :selenium, using: :firefox
+ #
+ # driven_by :selenium, screen_size: [800, 800]
+ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {})
+ @driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options)
+ end
+
+ # Returns the driver object for the initialized system test
+ def self.driver
+ @driver ||= SystemTestCase.driven_by(:selenium)
+ end
+ end
+
+ SystemTestCase.start_application
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
new file mode 100644
index 0000000000..5cf17883f7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -0,0 +1,34 @@
+module ActionDispatch
+ module SystemTesting
+ class Driver # :nodoc:
+ def initialize(name, **options)
+ @name = name
+ @browser = options[:using]
+ @screen_size = options[:screen_size]
+ @options = options[:options]
+ end
+
+ def use
+ register if selenium?
+ setup
+ end
+
+ private
+ def selenium?
+ @name == :selenium
+ end
+
+ def register
+ Capybara.register_driver @name do |app|
+ Capybara::Selenium::Driver.new(app, { browser: @browser }.merge(@options)).tap do |driver|
+ driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
+ end
+ end
+ end
+
+ def setup
+ Capybara.current_driver = @name
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb
new file mode 100644
index 0000000000..4a214ef713
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/server.rb
@@ -0,0 +1,32 @@
+require "rack/handler/puma"
+
+module ActionDispatch
+ module SystemTesting
+ class Server # :nodoc:
+ 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")
+ end
+ end
+
+ def setup
+ set_server
+ set_port
+ end
+
+ def set_server
+ Capybara.server = :rails_puma
+ end
+
+ def set_port
+ Capybara.always_include_port = true
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
new file mode 100644
index 0000000000..859d68e475
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -0,0 +1,94 @@
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ # Screenshot helper for system testing.
+ module ScreenshotHelper
+ # Takes a screenshot of the current page in the browser.
+ #
+ # +take_screenshot+ can be used at any point in your system tests to take
+ # a screenshot of the current state. This can be useful for debugging or
+ # automating visual testing.
+ #
+ # The screenshot will be displayed in your console, if supported.
+ #
+ # 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
+ # iTerm image protocol (http://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 format (http://buildkite.github.io/terminal/inline-images/).
+ def take_screenshot
+ save_image
+ puts display_image
+ end
+
+ # Takes a screenshot of the current page in the browser if the test
+ # failed.
+ #
+ # +take_failed_screenshot+ is included in <tt>application_system_test_case.rb</tt>
+ # that is generated with the application. To take screenshots when a test
+ # fails add +take_failed_screenshot+ to the teardown block before clearing
+ # sessions.
+ def take_failed_screenshot
+ take_screenshot if failed? && supports_screenshot?
+ end
+
+ private
+ def image_name
+ failed? ? "failures_#{method_name}" : method_name
+ end
+
+ def image_path
+ "tmp/screenshots/#{image_name}.png"
+ end
+
+ def save_image
+ page.save_screenshot(Rails.root.join(image_path))
+ end
+
+ def output_type
+ # 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"
+
+ output_type
+ end
+
+ def display_image
+ message = "[Screenshot]: #{image_path}\n"
+
+ case output_type
+ when "artifact"
+ message << "\e]1338;url=artifact://#{image_path}\a\n"
+ when "inline"
+ name = inline_base64(File.basename(image_path))
+ image = inline_base64(File.read(image_path))
+ message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n"
+ end
+
+ message
+ end
+
+ def inline_base64(path)
+ Base64.encode64(path).gsub("\n", "")
+ end
+
+ def failed?
+ !passed? && !skipped?
+ end
+
+ def supports_screenshot?
+ Capybara.current_driver != :rack_test
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..187ba2cc5f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -0,0 +1,20 @@
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module SetupAndTeardown # :nodoc:
+ DEFAULT_HOST = "127.0.0.1"
+
+ def before_setup
+ host! DEFAULT_HOST
+ super
+ end
+
+ def after_teardown
+ take_failed_screenshot
+ Capybara.reset_sessions!
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index b362931ef7..4ea18d671d 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -12,7 +12,7 @@ module ActionDispatch
include Rails::Dom::Testing::Assertions
def html_document
- @html_document ||= if @response.content_type.to_s =~ /xml\z/
+ @html_document ||= if @response.content_type.to_s.end_with?("xml")
Nokogiri::XML::Document.parse(@response.body)
else
Nokogiri::HTML::Document.parse(@response.body)
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 817737341c..1baf979ac9 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -45,7 +45,7 @@ module ActionDispatch
# # Asserts that the redirection was to the named route login_url
# assert_redirected_to login_url
#
- # # Asserts that the redirection was to the url for @customer
+ # # Asserts that the redirection was to the URL for @customer
# assert_redirected_to @customer
#
# # Asserts that the redirection matches the regular expression
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 454dcb9307..8645df4370 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -18,8 +18,8 @@ module ActionDispatch
# assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
#
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
- # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the
- # extras argument, appending the query string on the path directly will not work. For example:
+ # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
+ # argument because appending the query string on the path directly will not work. For example:
#
# # Asserts that a path of '/items/list/1?view=print' returns the correct options
# assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
@@ -132,8 +132,7 @@ module ActionDispatch
end
# A helper to make it easier to test different route configurations.
- # This method temporarily replaces @routes
- # with a new RouteSet instance.
+ # This method temporarily replaces @routes with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
# will create some routes using <tt>set.draw { match ... }</tt>:
@@ -152,8 +151,11 @@ module ActionDispatch
_routes = @routes
@controller.singleton_class.include(_routes.url_helpers)
- @controller.view_context_class = Class.new(@controller.view_context_class) do
- include _routes.url_helpers
+
+ if @controller.respond_to? :view_context_class
+ @controller.view_context_class = Class.new(@controller.view_context_class) do
+ include _routes.url_helpers
+ end
end
end
yield @routes
@@ -183,7 +185,6 @@ module ActionDispatch
method = :get
end
- # Assume given controller
request = ActionController::TestRequest.create @controller.class
if path =~ %r{://}
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index a1c2a8858a..2e2db98ad6 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -2,7 +2,6 @@ require "stringio"
require "uri"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/object/try"
-require "active_support/core_ext/string/strip"
require "rack/test"
require "minitest"
@@ -69,7 +68,7 @@ module ActionDispatch
DEFAULT_HOST = "www.example.com"
include Minitest::Assertions
- include RequestHelpers, Assertions
+ include TestProcess, RequestHelpers, Assertions
%w( status status_message headers body redirect? ).each do |method|
delegate method, to: :response, allow_nil: true
@@ -145,8 +144,8 @@ module ActionDispatch
self.host = DEFAULT_HOST
self.remote_addr = "127.0.0.1"
- self.accept = "text/xml,application/xml,application/xhtml+xml," +
- "text/html;q=0.9,text/plain;q=0.8,image/png," +
+ self.accept = "text/xml,application/xml,application/xhtml+xml," \
+ "text/html;q=0.9,text/plain;q=0.8,image/png," \
"*/*;q=0.5"
unless defined? @named_routes_configured
@@ -193,11 +192,10 @@ module ActionDispatch
# HTTP methods in integration tests. +#process+ is only required when using a
# request method that doesn't have a method defined in the integration tests.
#
- # This method returns a Response object, which one can use to
- # inspect the details of the response. Furthermore, if this method was
- # called from an ActionDispatch::IntegrationTest object, then that
- # object's <tt>@response</tt> instance variable will point to the same
- # response object.
+ # This method returns the response status, after performing the request.
+ # Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
+ # then that object's <tt>@response</tt> instance variable will point to a Response object
+ # which one can use to inspect the details of the response.
#
# Example:
# process :get, '/author', params: { since: 201501011400 }
@@ -211,7 +209,7 @@ module ActionDispatch
end
if path =~ %r{://}
- path = build_expanded_path(path, request_encoder) do |location|
+ path = build_expanded_path(path) do |location|
https! URI::HTTPS === location if location.scheme
if url_host = location.host
@@ -220,8 +218,6 @@ module ActionDispatch
host! url_host
end
end
- elsif as
- path = build_expanded_path(path, request_encoder)
end
hostname, port = host.split(":")
@@ -239,7 +235,7 @@ module ActionDispatch
"HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr,
"CONTENT_TYPE" => request_encoder.content_type,
- "HTTP_ACCEPT" => accept
+ "HTTP_ACCEPT" => request_encoder.accept_header || accept
}
wrapped_headers = Http::Headers.from_hash({})
@@ -250,7 +246,7 @@ module ActionDispatch
wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
end
- # this modifies the passed request_env directly
+ # This modifies the passed request_env directly.
if wrapped_headers.present?
Http::Headers.from_hash(request_env).merge!(wrapped_headers)
end
@@ -261,7 +257,7 @@ module ActionDispatch
session = Rack::Test::Session.new(_mock_session)
# NOTE: rack-test v0.5 doesn't build a default uri correctly
- # Make sure requested path is always a full uri
+ # Make sure requested path is always a full URI.
session.request(build_full_uri(path, request_env), request_env)
@request_count += 1
@@ -291,10 +287,10 @@ module ActionDispatch
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
end
- def build_expanded_path(path, request_encoder)
+ def build_expanded_path(path)
location = URI.parse(path)
yield location if block_given?
- path = request_encoder.append_format_to location.path
+ path = location.path
location.query ? "#{path}?#{location.query}" : path
end
end
@@ -328,8 +324,8 @@ module ActionDispatch
def create_session(app)
klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
- # If the app is a Rails app, make url_helpers available on the session
- # This makes app.url_for and app.foo_path available in the console
+ # If the app is a Rails app, make url_helpers available on the session.
+ # This makes app.url_for and app.foo_path available in the console.
if app.respond_to?(:routes)
include app.routes.url_helpers
include app.routes.mounted_helpers
@@ -575,17 +571,19 @@ module ActionDispatch
# end
#
# assert_response :success
- # assert_equal({ id: Arcticle.last.id, title: "Ahoy!" }, response.parsed_body)
+ # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
# end
# end
#
- # The +as+ option sets the format to JSON, sets the content type to
+ # The +as+ option passes an "application/json" Accept header (thereby setting
+ # the request format to JSON unless overridden), sets the content type to
# "application/json" and encodes the parameters as JSON.
#
# Calling +parsed_body+ on the response parses the response body based on the
# last response MIME type.
#
- # For any custom MIME types you've registered, you can even add your own encoders with:
+ # Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
+ # types you've registered, you can add your own encoders with:
#
# ActionDispatch::IntegrationTest.register_encoder :wibble,
# param_encoder: -> params { params.to_wibble },
@@ -598,7 +596,7 @@ module ActionDispatch
# Consult the Rails Testing Guide for more.
class IntegrationTest < ActiveSupport::TestCase
- include TestProcess
+ include TestProcess::FixtureFile
module UrlOptions
extend ActiveSupport::Concern
diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb
index b0b994b2d0..8c27e9ecb7 100644
--- a/actionpack/lib/action_dispatch/testing/request_encoder.rb
+++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb
@@ -1,10 +1,17 @@
module ActionDispatch
class RequestEncoder # :nodoc:
- @encoders = {}
+ class IdentityEncoder
+ def content_type; end
+ def accept_header; end
+ def encode_params(params); params; end
+ def response_parser; -> body { body }; end
+ end
+
+ @encoders = { identity: IdentityEncoder.new }
attr_reader :response_parser
- def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false)
+ def initialize(mime_name, param_encoder, response_parser)
@mime = Mime[mime_name]
unless @mime
@@ -12,21 +19,15 @@ module ActionDispatch
"unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
end
- @url_encoded_form = url_encoded_form
- @path_format = ".#{@mime.symbol}" unless @url_encoded_form
- @response_parser = response_parser || -> body { body }
- @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
+ @response_parser = response_parser || -> body { body }
+ @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
end
- def append_format_to(path)
- if @url_encoded_form
- path
- else
- path + @path_format
- end
+ def content_type
+ @mime.to_s
end
- def content_type
+ def accept_header
@mime.to_s
end
@@ -40,7 +41,7 @@ module ActionDispatch
end
def self.encoder(name)
- @encoders[name] || WWWFormEncoder
+ @encoders[name] || @encoders[:identity]
end
def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
@@ -48,7 +49,5 @@ module ActionDispatch
end
register_encoder :json, response_parser: -> body { JSON.parse(body) }
-
- WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true)
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 8b03b776fa..0282eb15c3 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -3,6 +3,26 @@ require "action_dispatch/middleware/flash"
module ActionDispatch
module TestProcess
+ module FixtureFile
+ # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
+ #
+ # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
+ #
+ # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
+ # This will not affect other platforms:
+ #
+ # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
+ def fixture_file_upload(path, mime_type = nil, binary = false)
+ if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
+ !File.exist?(path)
+ path = File.join(self.class.fixture_path, path)
+ end
+ Rack::Test::UploadedFile.new(path, mime_type, binary)
+ end
+ end
+
+ include FixtureFile
+
def assigns(key = nil)
raise NoMethodError,
"assigns has been extracted to a gem. To continue using it,
@@ -24,21 +44,5 @@ module ActionDispatch
def redirect_to_url
@response.redirect_url
end
-
- # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
- #
- # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
- #
- # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
- # This will not affect other platforms:
- #
- # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
- def fixture_file_upload(path, mime_type = nil, binary = false)
- if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
- !File.exist?(path)
- path = File.join(self.class.fixture_path, path)
- end
- Rack::Test::UploadedFile.new(path, mime_type, binary)
- end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index 91b25ec155..ec949c869b 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -9,7 +9,7 @@ module ActionDispatch
"HTTP_USER_AGENT" => "Rails Testing",
)
- # Create a new test request with default `env` values
+ # Create a new test request with default `env` values.
def self.create(env = {})
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
env["rack.request.cookie_hash"] ||= {}.with_indifferent_access
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index ee6dabd133..eec622e085 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2017 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/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index d8f86630b1..fddc3033d5 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -6,7 +6,7 @@ module ActionPack
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 11a9092527..4185ce1a1f 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -7,8 +7,8 @@ require "active_support/core_ext/kernel/reporting"
# These are the normal settings that will be set up by Railties
# TODO: Have these tests support other combinations of these values
silence_warnings do
- Encoding.default_internal = "UTF-8"
- Encoding.default_external = "UTF-8"
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
end
require "drb"
@@ -259,9 +259,9 @@ module ActionDispatch
host = uri_or_host.host unless path
path ||= uri_or_host.path
- params = { "PATH_INFO" => path,
- "REQUEST_METHOD" => method,
- "HTTP_HOST" => host }
+ params = { "PATH_INFO" => path,
+ "REQUEST_METHOD" => method,
+ "HTTP_HOST" => host }
routes.call(params)
end
@@ -354,15 +354,6 @@ class CommentsController < ResourcesController; end
class AccountsController < ResourcesController; end
class ImagesController < ResourcesController; end
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
-end
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
-end
-
require "active_support/testing/method_call_assertions"
class ForkingExecutor
@@ -438,4 +429,21 @@ end
class ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ private def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ private def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
+end
+
+class DrivenByRackTest < ActionDispatch::SystemTestCase
+ driven_by :rack_test
+end
+
+class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome
end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index b08f1f1707..9ab152fc5c 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -128,6 +128,16 @@ module Admin
end
end
+class ApiOnlyController < ActionController::API
+ def nothing
+ head :ok
+ end
+
+ def redirect_to_new_route
+ redirect_to new_route_url
+ end
+end
+
class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_render_file_absolute_path
get :render_file_absolute_path
@@ -170,6 +180,20 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
end
end
+ def test_with_routing_works_with_api_only_controllers
+ @controller = ApiOnlyController.new
+
+ with_routing do |set|
+ set.draw do
+ get "new_route", to: "api_only#nothing"
+ get "redirect_to_new_route", to: "api_only#redirect_to_new_route"
+ end
+
+ process :redirect_to_new_route
+ assert_redirected_to "http://test.host/new_route"
+ end
+ end
+
def test_assert_redirect_to_named_route_failure
with_routing do |set|
set.draw do
diff --git a/actionpack/test/controller/api/with_helpers_test.rb b/actionpack/test/controller/api/with_helpers_test.rb
new file mode 100644
index 0000000000..9882a25ae1
--- /dev/null
+++ b/actionpack/test/controller/api/with_helpers_test.rb
@@ -0,0 +1,26 @@
+require "abstract_unit"
+
+module ApiWithHelper
+ def my_helper
+ "helper"
+ end
+end
+
+class WithHelpersController < ActionController::API
+ include ActionController::Helpers
+ helper ApiWithHelper
+
+ def with_helpers
+ render plain: self.class.helpers.my_helper
+ end
+end
+
+class WithHelpersTest < ActionController::TestCase
+ tests WithHelpersController
+
+ def test_with_helpers
+ get :with_helpers
+
+ assert_equal "helper", response.body
+ end
+end
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index e0fa1fbab8..5f1463cfa8 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -55,7 +55,7 @@ class FilterTest < ActionController::TestCase
end
end
- protected
+ private
(1..3).each do |i|
define_method "try_#{i}" do
instance_variable_set :@try, i
@@ -296,7 +296,7 @@ class FilterTest < ActionController::TestCase
render inline: "ran action"
end
- protected
+ private
def find_user
@ran_filter ||= []
@ran_filter << "find_user"
@@ -428,7 +428,7 @@ class FilterTest < ActionController::TestCase
render plain: "bar"
end
- protected
+ private
def first
@first = true
end
@@ -584,7 +584,7 @@ class FilterTest < ActionController::TestCase
assert @controller.instance_variable_get(:@was_audited)
end
- def test_running_anomolous_yet_valid_condition_actions
+ def test_running_anomalous_yet_valid_condition_actions
test_process(AnomolousYetValidConditionController)
assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
assert @controller.instance_variable_get(:@ran_class_action)
@@ -704,7 +704,7 @@ class FilterTest < ActionController::TestCase
def test_prepending_and_appending_around_action
test_process(MixedFilterController)
- assert_equal " before aroundfilter before procfilter before appended aroundfilter " +
+ assert_equal " before aroundfilter before procfilter before appended aroundfilter " \
" after appended aroundfilter after procfilter after aroundfilter ",
MixedFilterController.execution_log
end
@@ -1040,7 +1040,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase
assert_equal 3, controller.instance_variable_get(:@try)
end
- protected
+ private
def test_process(controller, action = "show")
@controller = controller.is_a?(Class) ? controller.new : controller
process(action)
diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb
index 6b3abdd5be..45b598a594 100644
--- a/actionpack/test/controller/flash_hash_test.rb
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -63,10 +63,10 @@ module ActionDispatch
assert_equal({ "flashes" => { "foo" => "bar" }, "discard" => [] }, @hash.to_session_value)
@hash.discard("foo")
- assert_equal(nil, @hash.to_session_value)
+ assert_nil(@hash.to_session_value)
@hash.sweep
- assert_equal(nil, @hash.to_session_value)
+ assert_nil(@hash.to_session_value)
end
def test_from_session_value
@@ -75,7 +75,7 @@ module ActionDispatch
session = Marshal.load(Base64.decode64(rails_3_2_cookie))
hash = Flash::FlashHash.from_session_value(session["flash"])
assert_equal({ "greeting" => "Hello" }, hash.to_hash)
- assert_equal(nil, hash.to_session_value)
+ assert_nil(hash.to_session_value)
end
def test_from_session_value_on_json_serializer
@@ -84,7 +84,7 @@ module ActionDispatch
hash = Flash::FlashHash.from_session_value(session["flash"])
assert_equal({ "greeting" => "Hello" }, hash.to_hash)
- assert_equal(nil, hash.to_session_value)
+ assert_nil(hash.to_session_value)
assert_equal "Hello", hash[:greeting]
assert_equal "Hello", hash["greeting"]
end
diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb
index 3842136682..09d2793c9a 100644
--- a/actionpack/test/controller/http_token_authentication_test.rb
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -166,8 +166,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
test "token_and_options returns nil with no value after the equal sign" do
actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first
- expected = nil
- assert_equal(expected, actual)
+ assert_nil actual
end
test "raw_params returns a tuple of two key value pair strings" do
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index f89cfdb78c..57f58fd835 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -145,7 +145,7 @@ class IntegrationTestTest < ActiveSupport::TestCase
name.to_s == "foo" ? "pass" : super
end
end
- @test.class.superclass.__send__(:include, mixin)
+ @test.class.superclass.include(mixin)
begin
assert_equal "pass", @test.foo
ensure
@@ -269,8 +269,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
test "response cookies are added to the cookie jar for the next request" do
with_test_route_set do
- self.cookies["cookie_1"] = "sugar"
- self.cookies["cookie_2"] = "oatmeal"
+ cookies["cookie_1"] = "sugar"
+ cookies["cookie_2"] = "oatmeal"
get "/cookie_monster"
assert_equal "cookie_1=; path=/\ncookie_3=chocolate; path=/", headers["Set-Cookie"]
assert_equal({ "cookie_1" => "", "cookie_2" => "oatmeal", "cookie_3" => "chocolate" }, cookies.to_hash)
@@ -289,7 +289,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal "bar", body
- assert_equal nil, headers["Set-Cookie"]
+ assert_nil headers["Set-Cookie"]
assert_equal({ "foo" => "bar" }, cookies.to_hash)
end
end
@@ -308,7 +308,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal "bar", body
- assert_equal nil, headers["Set-Cookie"]
+ assert_nil headers["Set-Cookie"]
assert_equal({ "foo" => "bar" }, cookies.to_hash)
end
end
@@ -494,7 +494,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_includes @response.headers, "c"
end
- def test_accept_not_overriden_when_xhr_true
+ def test_accept_not_overridden_when_xhr_true
with_test_route_set do
get "/get", headers: { "Accept" => "application/json" }, xhr: true
assert_equal "application/json", request.accept
@@ -930,6 +930,10 @@ end
class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
+ def foos
+ render plain: "ok"
+ end
+
def foos_json
render json: params.permit(:foo)
end
@@ -958,13 +962,30 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
def test_encoding_as_json
post_to_foos as: :json do
assert_response :success
- assert_match "foos_json.json", request.path
assert_equal "application/json", request.content_type
+ assert_equal "application/json", request.accepts.first.to_s
+ assert_equal :json, request.format.ref
assert_equal({ "foo" => "fighters" }, request.request_parameters)
assert_equal({ "foo" => "fighters" }, response.parsed_body)
end
end
+ def test_doesnt_mangle_request_path
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ":action" => FooController
+ end
+ end
+
+ post "/foos"
+ assert_equal "/foos", request.path
+
+ post "/foos_json", as: :json
+ assert_equal "/foos_json", request.path
+ end
+ end
+
def test_encoding_as_without_mime_registration
assert_raise ArgumentError do
ActionDispatch::IntegrationTest.register_encoder :wibble
@@ -979,8 +1000,10 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
post_to_foos as: :wibble do
assert_response :success
- assert_match "foos_wibble.wibble", request.path
+ assert_equal "/foos_wibble", request.path
assert_equal "text/wibble", request.content_type
+ assert_equal "text/wibble", request.accepts.first.to_s
+ assert_equal :wibble, request.format.ref
assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed.
assert_equal "ok", response.parsed_body
end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index e76628b936..581081dd07 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -1,4 +1,5 @@
require "abstract_unit"
+require "timeout"
require "concurrent/atomic/count_down_latch"
Thread.abort_on_exception = true
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
index a2834c9f39..a22fa39051 100644
--- a/actionpack/test/controller/mime/accept_format_test.rb
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -40,7 +40,7 @@ class PostController < AbstractPostController
respond_to(:html, :iphone, :js)
end
-protected
+private
def with_iphone
request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
index c5f8165d04..61bd5c80c4 100644
--- a/actionpack/test/controller/mime/respond_to_test.rb
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -273,7 +273,7 @@ class RespondToController < ActionController::Base
end
end
- protected
+ private
def set_layout
case action_name
when "all_types_with_layout", "iphone_with_html_response_type"
@@ -519,7 +519,7 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal "Whatever you ask for, I got it", @response.body
end
- def test_handle_any_any_unkown_format
+ def test_handle_any_any_unknown_format
get :handle_any_any, format: "php"
assert_equal "Whatever you ask for, I got it", @response.body
end
diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
index 9956f0b107..054757fab3 100644
--- a/actionpack/test/controller/new_base/bare_metal_test.rb
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -52,7 +52,7 @@ module BareMetalTest
controller.set_request!(ActionDispatch::Request.empty)
controller.set_response!(BareController.make_response!(controller.request))
controller.index
- assert_equal nil, controller.response_body
+ assert_nil controller.response_body
end
end
diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb
index 3765d406fc..b891df4c0f 100644
--- a/actionpack/test/controller/new_base/base_test.rb
+++ b/actionpack/test/controller/new_base/base_test.rb
@@ -25,7 +25,7 @@ module Dispatching
render body: "actions: #{action_methods.to_a.sort.join(', ')}"
end
- protected
+ private
def authenticate
end
end
diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb
index b64468a94e..25b73ac78c 100644
--- a/actionpack/test/controller/new_base/render_context_test.rb
+++ b/actionpack/test/controller/new_base/render_context_test.rb
@@ -26,16 +26,14 @@ module RenderContext
render action: "hello_world", layout: "basic"
end
- protected
-
- # 3) Set view_context to self
- def view_context
- self
- end
+ protected def __controller_method__
+ "controller context!"
+ end
- def __controller_method__
- "controller context!"
- end
+ # 3) Set view_context to self
+ private def view_context
+ self
+ end
end
class RenderContextTest < Rack::TestCase
diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb
index 64b799f826..1177b8b03e 100644
--- a/actionpack/test/controller/new_base/render_streaming_test.rb
+++ b/actionpack/test/controller/new_base/render_streaming_test.rb
@@ -101,12 +101,12 @@ module RenderStreaming
assert_body "Hello world, I'm here!"
assert_status 200
assert_equal "22", headers["Content-Length"]
- assert_equal nil, headers["Transfer-Encoding"]
+ assert_nil headers["Transfer-Encoding"]
end
def assert_streaming!(cache = "no-cache")
assert_status 200
- assert_equal nil, headers["Content-Length"]
+ assert_nil headers["Content-Length"]
assert_equal "chunked", headers["Transfer-Encoding"]
assert_equal cache, headers["Cache-Control"]
end
diff --git a/actionpack/test/controller/parameter_encoding_test.rb b/actionpack/test/controller/parameter_encoding_test.rb
index 7840b4f5c4..234d0bddd1 100644
--- a/actionpack/test/controller/parameter_encoding_test.rb
+++ b/actionpack/test/controller/parameter_encoding_test.rb
@@ -1,9 +1,8 @@
require "abstract_unit"
class ParameterEncodingController < ActionController::Base
- parameter_encoding :test_bar, :bar, Encoding::ASCII_8BIT
- parameter_encoding :test_baz, :baz, Encoding::ISO_8859_1
- parameter_encoding :test_baz_to_ascii, :baz, Encoding::ASCII_8BIT
+ skip_parameter_encoding :test_bar
+ skip_parameter_encoding :test_all_values_encoding
def test_foo
render body: params[:foo].encoding
@@ -13,16 +12,8 @@ class ParameterEncodingController < ActionController::Base
render body: params[:bar].encoding
end
- def test_baz
- render body: params[:baz].encoding
- end
-
- def test_no_change_to_baz
- render body: params[:baz].encoding
- end
-
- def test_baz_to_ascii
- render body: params[:baz].encoding
+ def test_all_values_encoding
+ render body: ::JSON.dump(params.values.map(&:encoding).map(&:name))
end
end
@@ -36,32 +27,18 @@ class ParameterEncodingTest < ActionController::TestCase
assert_equal "UTF-8", @response.body
end
- test "properly transcodes ASCII_8BIT parameters into declared encodings" do
+ test "properly encodes ASCII_8BIT parameters into binary" do
post :test_bar, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
assert_response :success
assert_equal "ASCII-8BIT", @response.body
end
- test "properly transcodes ISO_8859_1 parameters into declared encodings" do
- post :test_baz, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
-
- assert_response :success
- assert_equal "ISO-8859-1", @response.body
- end
-
- test "does not transcode parameters when not specified" do
- post :test_no_change_to_baz, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
+ test "properly encodes all ASCII_8BIT parameters into binary" do
+ post :test_all_values_encoding, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
assert_response :success
- assert_equal "UTF-8", @response.body
- end
-
- test "respects different encoding declarations for a param per action" do
- post :test_baz_to_ascii, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
-
- assert_response :success
- assert_equal "ASCII-8BIT", @response.body
+ assert_equal ["ASCII-8BIT"], JSON.parse(@response.body).uniq
end
test "does not raise an error when passed a param declared as ASCII-8BIT that contains invalid bytes" do
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index 2893eb7b91..7725c25e22 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -53,6 +53,15 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "empty? returns true when params contains no key/value pairs" do
+ params = ActionController::Parameters.new
+ assert params.empty?
+ end
+
+ test "empty? returns false when any params are present" do
+ refute @params.empty?
+ end
+
test "except retains permitted status" do
@params.permit!
assert @params.except(:person).permitted?
@@ -75,6 +84,45 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert_not @params[:person].fetch(:name).permitted?
end
+ test "has_key? returns true if the given key is present in the params" do
+ assert @params.has_key?(:person)
+ end
+
+ test "has_key? returns false if the given key is not present in the params" do
+ refute @params.has_key?(:address)
+ end
+
+ test "has_value? returns true if the given value is present in the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert params.has_value?("Chicago")
+ end
+
+ 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")
+ end
+
+ test "include? returns true if the given key is present in the params" do
+ assert @params.include?(:person)
+ end
+
+ test "include? returns false if the given key is not present in the params" do
+ refute @params.include?(:address)
+ end
+
+ test "key? returns true if the given key is present in the params" do
+ assert @params.key?(:person)
+ end
+
+ test "key? returns false if the given key is not present in the params" do
+ refute @params.key?(:address)
+ end
+
+ test "keys returns an array of the keys of the params" do
+ assert_equal ["person"], @params.keys
+ assert_equal ["age", "name", "addresses"], @params[:person].keys
+ end
+
test "reject retains permitted status" do
assert_not @params.reject { |k| k == "person" }.permitted?
end
@@ -120,6 +168,21 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert_not @params.transform_values { |v| v }.permitted?
end
+ test "value? returns true if the given value is present in the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert params.value?("Chicago")
+ end
+
+ 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")
+ end
+
+ test "values returns an array of the values of the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert_equal ["Chicago", "Illinois"], params.values
+ end
+
test "values_at retains permitted status" do
@params.permit!
assert @params.values_at(:person).first.permitted?
diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
index 5f86901e30..00e591d5a7 100644
--- a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
@@ -140,6 +140,11 @@ class NestedParametersPermitTest < ActiveSupport::TestCase
assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["0"][:name]
assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["1"][:name]
+ assert_equal(
+ { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare" }, "1" => { "name" => "Unattributed Assistant" }, "2" => {} } } },
+ permitted.to_h
+ )
+
assert_filtered_out permitted[:book][:authors_attributes]["0"], :age_of_death
end
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index 92e134d313..9f3025587e 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -66,12 +66,20 @@ class ParametersPermitTest < ActiveSupport::TestCase
values.each do |value|
params = ActionController::Parameters.new(id: value)
permitted = params.permit(:id)
- assert_equal value, permitted[:id]
+ if value.nil?
+ assert_nil permitted[:id]
+ else
+ assert_equal value, permitted[:id]
+ end
@struct_fields.each do |sf|
params = ActionController::Parameters.new(sf => value)
permitted = params.permit(:sf)
- assert_equal value, permitted[sf]
+ if value.nil?
+ assert_nil permitted[sf]
+ else
+ assert_equal value, permitted[sf]
+ end
end
end
end
@@ -141,7 +149,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
permitted = params.permit(:a, c: [], b: [])
assert_equal 1, permitted[:a]
assert_equal [1, 2, 3], permitted[:b]
- assert_equal nil, permitted[:c]
+ assert_nil permitted[:c]
end
test "key to empty array: arrays of permitted scalars pass" do
@@ -187,11 +195,6 @@ class ParametersPermitTest < ActiveSupport::TestCase
permitted = params.permit(:username, preferences: {}, hacked: {})
- assert permitted.permitted?
- assert permitted[:preferences].permitted?
- assert permitted[:preferences][:font].permitted?
- assert permitted[:preferences][:dubious].all?(&:permitted?)
-
assert_equal "fxn", permitted[:username]
assert_equal "Marazul", permitted[:preferences][:scheme]
assert_equal "Source Code Pro", permitted[:preferences][:font][:name]
@@ -216,7 +219,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
test "fetch with a default value of a hash does not mutate the object" do
params = ActionController::Parameters.new({})
params.fetch :foo, {}
- assert_equal nil, params[:foo]
+ assert_nil params[:foo]
end
test "hashes in array values get wrapped" do
@@ -254,8 +257,8 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do
- assert_equal nil, @params.fetch(:foo, nil)
- assert_equal nil, @params.fetch(:foo) { nil }
+ assert_nil @params.fetch(:foo, nil)
+ assert_nil @params.fetch(:foo) { nil }
end
test "KeyError in fetch block should not be covered up" do
@@ -299,6 +302,31 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "32", @params[:person][:age]
end
+ test "#reverse_merge with parameters" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ merged_params = @params.reverse_merge(default_params)
+
+ assert_equal "1234", merged_params[:id]
+ refute_predicate merged_params[:person], :empty?
+ end
+
+ test "not permitted is sticky beyond reverse_merge" do
+ refute_predicate @params.reverse_merge(a: "b"), :permitted?
+ end
+
+ test "permitted is sticky beyond reverse_merge" do
+ @params.permit!
+ assert_predicate @params.reverse_merge(a: "b"), :permitted?
+ end
+
+ test "#reverse_merge! with parameters" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ @params.reverse_merge!(default_params)
+
+ assert_equal "1234", @params[:id]
+ refute_predicate @params[:person], :empty?
+ end
+
test "modifying the parameters" do
@params[:person][:hometown] = "Chicago"
@params[:person][:family] = { brother: "Jonas" }
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index 46a2ab8ccf..2a41d57b26 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -32,6 +32,10 @@ class ParamsWrapperTest < ActionController::TestCase
def self.attribute_names
[]
end
+
+ def self.stored_attributes
+ { settings: [:color, :size] }
+ end
end
class Person
@@ -50,7 +54,7 @@ class ParamsWrapperTest < ActionController::TestCase
with_default_wrapper_options do
@request.env["CONTENT_TYPE"] = "application/json"
post :parse, params: { "username" => "sikachu" }
- assert_equal @request.filtered_parameters, "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" }
+ assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" } }, @request.filtered_parameters)
end
end
@@ -62,6 +66,17 @@ class ParamsWrapperTest < ActionController::TestCase
end
end
+ def test_store_accessors_wrapped
+ 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", "color" => "blue", "size" => "large" }
+ assert_parameters("username" => "sikachu", "color" => "blue", "size" => "large",
+ "user" => { "username" => "sikachu", "color" => "blue", "size" => "large" })
+ end
+ end
+ end
+
def test_specify_wrapper_name
with_default_wrapper_options do
UsersController.wrap_parameters :person
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 0e61b92bcf..f06a1f4d23 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -21,8 +21,8 @@ end
class RedirectController < ActionController::Base
# empty method not used anywhere to ensure methods like
# `status` and `location` aren't called on `redirect_to` calls
- def status; render plain: "called status"; end
- def location; render plain: "called location"; end
+ def status; raise "Should not be called!"; end
+ def location; raise "Should not be called!"; end
def simple_redirect
redirect_to action: "hello_world"
@@ -123,7 +123,7 @@ class RedirectController < ActionController::Base
def rescue_errors(e) raise e end
- protected
+ private
def dashbord_url(id, message)
url_for action: "dashboard", params: { "id" => id, "message" => message }
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 0c0f18f200..3a0a0a8bde 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -313,7 +313,6 @@ class ExpiresInRenderTest < ActionController::TestCase
end
def test_permitted_dynamic_render_file_hash
- skip "FIXME: this test passes on 4-2-stable but not master. Why?"
assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb"))
response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } }
assert_equal File.read(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")),
diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb
index 866600b935..052c974d68 100644
--- a/actionpack/test/controller/renderer_test.rb
+++ b/actionpack/test/controller/renderer_test.rb
@@ -19,6 +19,16 @@ class RendererTest < ActiveSupport::TestCase
assert_equal controller, renderer.controller
end
+ test "creating with new defaults" do
+ renderer = ApplicationController.renderer
+
+ new_defaults = { https: true }
+ new_renderer = renderer.with_defaults(new_defaults).new
+ content = new_renderer.render(inline: "<%= request.ssl? %>")
+
+ assert_equal "true", content
+ end
+
test "rendering with a class renderer" do
renderer = ApplicationController.renderer
content = renderer.render template: "ruby_template"
@@ -103,6 +113,20 @@ class RendererTest < ActiveSupport::TestCase
assert_equal "true", content
end
+ test "return valid asset url with defaults" do
+ renderer = ApplicationController.renderer
+ content = renderer.render inline: "<%= asset_url 'asset.jpg' %>"
+
+ assert_equal "http://example.org/asset.jpg", content
+ end
+
+ test "return valid asset url when https is true" do
+ renderer = ApplicationController.renderer.new https: true
+ content = renderer.render inline: "<%= asset_url 'asset.jpg' %>"
+
+ assert_equal "https://example.org/asset.jpg", content
+ end
+
private
def render
@render ||= ApplicationController.renderer.method(:render)
diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb
index da6fa1388b..1440db00f6 100644
--- a/actionpack/test/controller/request/test_request_test.rb
+++ b/actionpack/test/controller/request/test_request_test.rb
@@ -8,7 +8,7 @@ class ActionController::TestRequestTest < ActionController::TestCase
def test_mutating_session_options_does_not_affect_default_options
@request.session_options[:myparam] = 123
- assert_equal nil, ActionController::TestSession::DEFAULT_OPTIONS[:myparam]
+ assert_nil ActionController::TestSession::DEFAULT_OPTIONS[:myparam]
end
def test_content_length_has_bytes_count_value
@@ -17,20 +17,24 @@ class ActionController::TestRequestTest < ActionController::TestCase
@request.set_header "CONTENT_TYPE", "application/json"
@request.assign_parameters(@routes, "test", "create", non_ascii_parameters,
"/test", [:data, :controller, :action])
- assert_equal(@request.get_header("CONTENT_LENGTH"),
- StringIO.new(non_ascii_parameters.to_json).length.to_s)
+ assert_equal(StringIO.new(non_ascii_parameters.to_json).length.to_s,
+ @request.get_header("CONTENT_LENGTH"))
end
- ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_key do |option|
- test "rack default session options #{option} exists in session options and is default" do
- assert_equal(ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS[option],
- @request.session_options[option],
- "Missing rack session default option #{option} in request.session_options")
+ ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_pair do |key, value|
+ test "rack default session options #{key} exists in session options and is default" do
+ if value.nil?
+ assert_nil(@request.session_options[key],
+ "Missing rack session default option #{key} in request.session_options")
+ else
+ assert_equal(value, @request.session_options[key],
+ "Missing rack session default option #{key} in request.session_options")
+ end
end
- test "rack default session options #{option} exists in session options" do
- assert(@request.session_options.has_key?(option),
- "Missing rack session option #{option} in request.session_options")
+ test "rack default session options #{key} exists in session options" do
+ assert(@request.session_options.has_key?(key),
+ "Missing rack session option #{key} in request.session_options")
end
end
end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 90d5ab3c67..d645ddfdbe 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -92,7 +92,7 @@ class PrependProtectForgeryBaseController < ActionController::Base
render inline: "OK"
end
- protected
+ private
def add_called_callback(name)
@called_callbacks ||= []
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
index a98e6479b7..9ae22c4554 100644
--- a/actionpack/test/controller/rescue_test.rb
+++ b/actionpack/test/controller/rescue_test.rb
@@ -149,7 +149,7 @@ class RescueController < ActionController::Base
raise RangeError
end
- protected
+ private
def deny_access
head :forbidden
end
@@ -327,7 +327,7 @@ class RescueTest < ActionDispatch::IntegrationTest
raise "b00m"
end
- protected
+ private
def show_errors(exception)
render plain: exception.message
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 0b3dc6c41f..fad34dacce 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1089,7 +1089,7 @@ class ResourcesTest < ActionController::TestCase
end
end
- protected
+ private
def with_restful_routing(*args)
options = args.extract_options!
collection_methods = options.delete(:collection)
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index a28283f4d6..9e6b975fe2 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -241,10 +241,17 @@ class SendFileTest < ActionController::TestCase
assert_equal "text/calendar; charset=utf-8", response.headers["Content-Type"]
end
+ def test_send_file_charset_with_type_options_key_without_charset
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { type: "image/png" }
+ response = process("file")
+ assert_equal "image/png", response.headers["Content-Type"]
+ end
+
def test_send_file_charset_with_content_type_options_key
@controller = SendFileWithActionControllerLive.new
@controller.options = { content_type: "text/calendar" }
response = process("file")
- assert_equal "text/calendar; charset=utf-8", response.headers["Content-Type"]
+ assert_equal "text/calendar", response.headers["Content-Type"]
end
end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 05aa4ff6ad..3a4307b64b 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -100,11 +100,11 @@ HTML
end
def test_xml_output
- response.content_type = "application/xml"
+ response.content_type = params[:response_as]
render plain: <<XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
- <area>area is an empty tag in HTML, raising an error if not in xml mode</area>
+ <area><p>area is an empty tag in HTML, so it won't contain this content</p></area>
</root>
XML
end
@@ -134,7 +134,7 @@ XML
end
def create
- head :created, location: "created resource"
+ head :created, location: "/resource"
end
def render_cookie
@@ -374,18 +374,18 @@ XML
assert_equal "OK", @response.body
end
- def test_should_not_impose_childless_html_tags_in_xml
- process :test_xml_output
+ def test_should_impose_childless_html_tags_in_html
+ process :test_xml_output, params: { response_as: "text/html" }
- begin
- $stderr = StringIO.new
- assert_select "area" #This will cause a warning if content is processed as HTML
- $stderr.rewind && err = $stderr.read
- ensure
- $stderr = STDERR
- end
+ # <area> auto-closes, so the <p> becomes a sibling
+ assert_select "root > area + p"
+ end
+
+ def test_should_not_impose_childless_html_tags_in_xml
+ process :test_xml_output, params: { response_as: "application/xml" }
- assert err.empty?, err.inspect
+ # <area> is not special, so the <p> is its child
+ assert_select "root > area > p"
end
def test_assert_generates
@@ -679,6 +679,11 @@ XML
assert_equal "baz", @request.filtered_parameters[:foo]
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
+ end
+
def test_path_params_reset_between_request
get :test_params, params: { id: "foo" }
assert_equal "foo", @request.path_parameters[:id]
@@ -796,7 +801,6 @@ XML
new_content_type = "new content_type"
file.content_type = new_content_type
assert_equal new_content_type, file.content_type
-
end
def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case
@@ -880,12 +884,12 @@ XML
assert_response :created
# Redirect url doesn't care that it wasn't a :redirect response.
- assert_equal "created resource", @response.redirect_url
+ assert_equal "/resource", @response.redirect_url
assert_equal @response.redirect_url, redirect_to_url
# Must be a :redirect response.
assert_raise(ActiveSupport::TestCase::Assertion) do
- assert_redirected_to "created resource"
+ assert_redirected_to "/resource"
end
end
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index 4b6f33c545..862dcf01c3 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -26,7 +26,7 @@ module AbstractController
action: :index
}
}.url_helpers
- self.default_url_options[:host] = "example.com"
+ default_url_options[:host] = "example.com"
}
path = klass.new.fun_path(controller: :articles,
@@ -487,6 +487,27 @@ module AbstractController
end
end
+ def test_default_params_first_empty
+ with_routing do |set|
+ set.draw do
+ get "(:param1)/test(/:param2)" => "index#index",
+ defaults: {
+ param1: 1,
+ param2: 2
+ },
+ constraints: {
+ param1: /\d*/,
+ param2: /\d+/
+ }
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ assert_equal "http://www.basecamphq.com/test", kls.new.url_for(controller: "index", param1: "1")
+ end
+ end
+
private
def extract_params(url)
url.split("?", 2).last.split("&").sort
diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb
index 57e21a22c6..29a5dfc0ad 100644
--- a/actionpack/test/dispatch/callbacks_test.rb
+++ b/actionpack/test/dispatch/callbacks_test.rb
@@ -35,24 +35,6 @@ class DispatcherTest < ActiveSupport::TestCase
assert_equal 6, Foo.b
end
- def test_to_prepare_and_cleanup_delegation
- prepared = cleaned = false
- assert_deprecated do
- ActionDispatch::Callbacks.to_prepare { prepared = true }
- ActionDispatch::Callbacks.to_prepare { cleaned = true }
- end
-
- assert_deprecated do
- ActionDispatch::Reloader.prepare!
- end
- assert prepared
-
- assert_deprecated do
- ActionDispatch::Reloader.cleanup!
- end
- assert cleaned
- end
-
private
def dispatch(&block)
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index af3036d448..664faa31bb 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -272,6 +272,10 @@ class CookiesTest < ActionController::TestCase
def noop
head :ok
end
+
+ def encrypted_cookie
+ cookies.encrypted["foo"]
+ end
end
tests TestController
@@ -940,8 +944,8 @@ class CookiesTest < ActionController::TestCase
@request.headers["Cookie"] = "user_id=45"
get :get_signed_cookie
- assert_equal nil, @controller.send(:cookies).signed[:user_id]
- assert_equal nil, @response.cookies["user_id"]
+ assert_nil @controller.send(:cookies).signed[:user_id]
+ assert_nil @response.cookies["user_id"]
end
def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered
@@ -951,8 +955,8 @@ class CookiesTest < ActionController::TestCase
@request.headers["Cookie"] = "foo=baz"
get :get_encrypted_cookie
- assert_equal nil, @controller.send(:cookies).encrypted[:foo]
- assert_equal nil, @response.cookies["foo"]
+ assert_nil @controller.send(:cookies).encrypted[:foo]
+ assert_nil @response.cookies["foo"]
end
def test_cookie_with_all_domain_option
@@ -1189,6 +1193,12 @@ class CookiesTest < ActionController::TestCase
assert_equal "david", cookies[:user_name]
end
+ def test_cookies_are_not_cleared
+ cookies.encrypted["foo"] = "bar"
+ get :noop
+ assert_equal "bar", @controller.encrypted_cookie
+ end
+
private
def assert_cookie_header(expected)
header = @response.headers["Set-Cookie"]
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index bb2fc53add..0e093d2188 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -22,8 +22,6 @@ module TestGenerationPrefix
end
class WithMountedEngine < ActionDispatch::IntegrationTest
- include Rack::Test::Methods
-
class BlogEngine < Rails::Engine
routes.draw do
get "/posts/:id", to: "inside_engine_generating#show", as: :post
@@ -153,114 +151,114 @@ module TestGenerationPrefix
# Inside Engine
test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do
get "/pure-awesomeness/blog/posts/1"
- assert_equal "/pure-awesomeness/blog/posts/1", last_response.body
+ assert_equal "/pure-awesomeness/blog/posts/1", response.body
end
test "[ENGINE] generating application's url never uses SCRIPT_NAME from request" do
get "/pure-awesomeness/blog/url_to_application"
- assert_equal "/generate", last_response.body
+ assert_equal "/generate", response.body
end
test "[ENGINE] generating engine's url with polymorphic path" do
get "/pure-awesomeness/blog/polymorphic_path_for_engine"
- assert_equal "/pure-awesomeness/blog/posts/1", last_response.body
+ assert_equal "/pure-awesomeness/blog/posts/1", response.body
end
test "[ENGINE] url_helpers from engine have higher priority than application's url_helpers" do
get "/awesome/blog/conflicting_url"
- assert_equal "engine", last_response.body
+ assert_equal "engine", response.body
end
test "[ENGINE] relative path root uses SCRIPT_NAME from request" do
get "/awesome/blog/relative_path_root"
- verify_redirect "http://example.org/awesome/blog"
+ verify_redirect "http://www.example.com/awesome/blog"
end
test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
get "/awesome/blog/relative_path_redirect"
- verify_redirect "http://example.org/awesome/blog/foo"
+ verify_redirect "http://www.example.com/awesome/blog/foo"
end
test "[ENGINE] relative option root uses SCRIPT_NAME from request" do
get "/awesome/blog/relative_option_root"
- verify_redirect "http://example.org/awesome/blog"
+ verify_redirect "http://www.example.com/awesome/blog"
end
test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
get "/awesome/blog/relative_option_redirect"
- verify_redirect "http://example.org/awesome/blog/foo"
+ verify_redirect "http://www.example.com/awesome/blog/foo"
end
test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do
get "/awesome/blog/relative_custom_root"
- verify_redirect "http://example.org/awesome/blog"
+ verify_redirect "http://www.example.com/awesome/blog"
end
test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
get "/awesome/blog/relative_custom_redirect"
- verify_redirect "http://example.org/awesome/blog/foo"
+ verify_redirect "http://www.example.com/awesome/blog/foo"
end
test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do
get "/awesome/blog/absolute_path_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
get "/awesome/blog/absolute_path_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do
get "/awesome/blog/absolute_option_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
get "/awesome/blog/absolute_option_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do
get "/awesome/blog/absolute_custom_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
get "/awesome/blog/absolute_custom_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
# Inside Application
test "[APP] generating engine's route includes prefix" do
get "/generate"
- assert_equal "/awesome/blog/posts/1", last_response.body
+ assert_equal "/awesome/blog/posts/1", response.body
end
test "[APP] generating engine's route includes default_url_options[:script_name]" do
RailsApplication.routes.default_url_options = { script_name: "/something" }
get "/generate"
- assert_equal "/something/awesome/blog/posts/1", last_response.body
+ assert_equal "/something/awesome/blog/posts/1", response.body
end
test "[APP] generating engine's url with polymorphic path" do
get "/polymorphic_path_for_engine"
- assert_equal "/awesome/blog/posts/1", last_response.body
+ assert_equal "/awesome/blog/posts/1", response.body
end
test "polymorphic_path_for_app" do
get "/polymorphic_path_for_app"
- assert_equal "/posts/1", last_response.body
+ assert_equal "/posts/1", response.body
end
test "[APP] generating engine's url with url_for(@post)" do
get "/polymorphic_with_url_for"
- assert_equal "http://example.org/awesome/blog/posts/1", last_response.body
+ assert_equal "http://www.example.com/awesome/blog/posts/1", response.body
end
test "[APP] instance variable with same name as engine" do
get "/ivar_usage"
- assert_equal "/awesome/blog/posts/1", last_response.body
+ assert_equal "/awesome/blog/posts/1", response.body
end
# Inside any Object
@@ -322,9 +320,9 @@ module TestGenerationPrefix
private
def verify_redirect(url, status = 301)
- assert_equal status, last_response.status
- assert_equal url, last_response.headers["Location"]
- assert_equal expected_redirect_body(url), last_response.body
+ assert_equal status, response.status
+ assert_equal url, response.headers["Location"]
+ assert_equal expected_redirect_body(url), response.body
end
def expected_redirect_body(url)
@@ -333,8 +331,6 @@ module TestGenerationPrefix
end
class EngineMountedAtRoot < ActionDispatch::IntegrationTest
- include Rack::Test::Methods
-
class BlogEngine
def self.routes
@routes ||= begin
@@ -388,74 +384,74 @@ module TestGenerationPrefix
test "generating path inside engine" do
get "/posts/1"
- assert_equal "/posts/1", last_response.body
+ assert_equal "/posts/1", response.body
end
test "[ENGINE] relative path root uses SCRIPT_NAME from request" do
get "/relative_path_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
get "/relative_path_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] relative option root uses SCRIPT_NAME from request" do
get "/relative_option_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
get "/relative_option_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do
get "/relative_custom_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
get "/relative_custom_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do
get "/absolute_path_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
get "/absolute_path_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do
get "/absolute_option_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
get "/absolute_option_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do
get "/absolute_custom_root"
- verify_redirect "http://example.org/"
+ verify_redirect "http://www.example.com/"
end
test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
get "/absolute_custom_redirect"
- verify_redirect "http://example.org/foo"
+ verify_redirect "http://www.example.com/foo"
end
private
def verify_redirect(url, status = 301)
- assert_equal status, last_response.status
- assert_equal url, last_response.headers["Location"]
- assert_equal expected_redirect_body(url), last_response.body
+ assert_equal status, response.status
+ assert_equal url, response.headers["Location"]
+ assert_equal expected_redirect_body(url), response.body
end
def expected_redirect_body(url)
diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb
index db68549b84..9eb78fe059 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -1,32 +1,11 @@
require "abstract_unit"
class ReloaderTest < ActiveSupport::TestCase
- Reloader = ActionDispatch::Reloader
-
teardown do
ActiveSupport::Reloader.reset_callbacks :prepare
ActiveSupport::Reloader.reset_callbacks :complete
end
- def test_prepare_callbacks
- a = b = c = nil
- assert_deprecated do
- Reloader.to_prepare { |*args| a = b = c = 1 }
- Reloader.to_prepare { |*args| b = c = 2 }
- Reloader.to_prepare { |*args| c = 3 }
- end
-
- # Ensure to_prepare callbacks are not run when defined
- assert_nil a || b || c
-
- # Run callbacks
- call_and_return_body
-
- assert_equal 1, a
- assert_equal 2, b
- assert_equal 3, c
- end
-
class MyBody < Array
def initialize(&block)
@on_close = block
@@ -45,6 +24,23 @@ class ReloaderTest < ActiveSupport::TestCase
end
end
+ def test_prepare_callbacks
+ a = b = c = nil
+ reloader.to_prepare { |*args| a = b = c = 1 }
+ reloader.to_prepare { |*args| b = c = 2 }
+ reloader.to_prepare { |*args| c = 3 }
+
+ # Ensure to_prepare callbacks are not run when defined
+ assert_nil a || b || c
+
+ # Run callbacks
+ call_and_return_body
+
+ assert_equal 1, a
+ assert_equal 2, b
+ assert_equal 3, c
+ end
+
def test_returned_body_object_always_responds_to_close
body = call_and_return_body
assert_respond_to body, :close
@@ -62,15 +58,12 @@ class ReloaderTest < ActiveSupport::TestCase
def test_condition_specifies_when_to_reload
i, j = 0, 0, 0, 0
- assert_deprecated do
- Reloader.to_prepare { |*args| i += 1 }
- Reloader.to_cleanup { |*args| j += 1 }
- end
- x = Class.new(ActiveSupport::Reloader)
- x.check = lambda { i < 3 }
+ reloader = reloader(lambda { i < 3 })
+ reloader.to_prepare { |*args| i += 1 }
+ reloader.to_complete { |*args| j += 1 }
- app = Reloader.new(lambda { |env| [200, {}, []] }, x)
+ app = middleware(lambda { |env| [200, {}, []] }, reloader)
5.times do
resp = app.call({})
resp[2].close
@@ -115,24 +108,20 @@ class ReloaderTest < ActiveSupport::TestCase
assert_respond_to body, :bar
end
- def test_cleanup_callbacks_are_called_when_body_is_closed
- cleaned = false
- assert_deprecated do
- Reloader.to_cleanup { cleaned = true }
- end
+ def test_complete_callbacks_are_called_when_body_is_closed
+ completed = false
+ reloader.to_complete { completed = true }
body = call_and_return_body
- assert !cleaned
+ assert !completed
body.close
- assert cleaned
+ assert completed
end
def test_prepare_callbacks_arent_called_when_body_is_closed
prepared = false
- assert_deprecated do
- Reloader.to_prepare { prepared = true }
- end
+ reloader.to_prepare { prepared = true }
body = call_and_return_body
prepared = false
@@ -141,45 +130,9 @@ class ReloaderTest < ActiveSupport::TestCase
assert !prepared
end
- def test_manual_reloading
- prepared = cleaned = false
- assert_deprecated do
- Reloader.to_prepare { prepared = true }
- Reloader.to_cleanup { cleaned = true }
- end
-
- assert_deprecated do
- Reloader.prepare!
- end
- assert prepared
- assert !cleaned
-
- prepared = cleaned = false
- assert_deprecated do
- Reloader.cleanup!
- end
- assert prepared
- assert cleaned
- end
-
- def test_prepend_prepare_callback
- i = 10
- assert_deprecated do
- Reloader.to_prepare { i += 1 }
- Reloader.to_prepare(prepend: true) { i = 0 }
- end
-
- assert_deprecated do
- Reloader.prepare!
- end
- assert_equal 1, i
- end
-
- def test_cleanup_callbacks_are_called_on_exceptions
- cleaned = false
- assert_deprecated do
- Reloader.to_cleanup { cleaned = true }
- end
+ def test_complete_callbacks_are_called_on_exceptions
+ completed = false
+ reloader.to_complete { completed = true }
begin
call_and_return_body do
@@ -188,16 +141,25 @@ class ReloaderTest < ActiveSupport::TestCase
rescue
end
- assert cleaned
+ assert completed
end
private
def call_and_return_body(&block)
- x = Class.new(ActiveSupport::Reloader)
- x.check = lambda { true }
+ app = middleware(block || proc { [200, {}, "response"] })
+ _, _, body = app.call("rack.input" => StringIO.new(""))
+ body
+ end
+
+ def middleware(inner_app, reloader = reloader())
+ ActionDispatch::Reloader.new(inner_app, reloader)
+ end
- @response ||= "response"
- @reloader ||= Reloader.new(block || proc { [200, {}, @response] }, x)
- @reloader.call("rack.input" => StringIO.new(""))[2]
+ def reloader(check = lambda { true })
+ @reloader ||= begin
+ reloader = Class.new(ActiveSupport::Reloader)
+ reloader.check = check
+ reloader
+ end
end
end
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index eb4bb14ed1..01c5ff1429 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -129,7 +129,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
params = parse_multipart("none")
assert_equal %w(submit-name), params.keys.sort
assert_equal "Larry", params["submit-name"]
- assert_equal nil, params["files"]
+ assert_nil params["files"]
end
test "parses empty upload file" do
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 2f41851598..2f9228a62d 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -18,7 +18,7 @@ class BaseRequestTest < ActiveSupport::TestCase
ActionDispatch::Http::URL.url_for(options)
end
- protected
+ private
def stub_request(env = {})
ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true
@trusted_proxies ||= nil
@@ -94,13 +94,13 @@ class RequestIP < BaseRequestTest
assert_equal "3.4.5.6", request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,192.168.0.1"
- assert_equal nil, request.remote_ip
+ assert_nil request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "9.9.9.9, 3.4.5.6, 172.31.4.4, 10.0.0.1"
assert_equal "3.4.5.6", request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address"
- assert_equal nil, request.remote_ip
+ assert_nil request.remote_ip
end
test "remote ip spoof detection" do
@@ -154,7 +154,7 @@ class RequestIP < BaseRequestTest
assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,::1"
- assert_equal nil, request.remote_ip
+ assert_nil request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::, fc01::, fdff"
assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip
@@ -163,7 +163,7 @@ class RequestIP < BaseRequestTest
assert_equal "FE00::", request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address"
- assert_equal nil, request.remote_ip
+ assert_nil request.remote_ip
end
test "remote ip v6 spoof detection" do
@@ -200,7 +200,7 @@ class RequestIP < BaseRequestTest
assert_equal "3.4.5.6", request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "67.205.106.73,unknown"
- assert_equal nil, request.remote_ip
+ assert_nil request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "9.9.9.9, 3.4.5.6, 10.0.0.1, 67.205.106.73"
assert_equal "3.4.5.6", request.remote_ip
@@ -222,7 +222,7 @@ class RequestIP < BaseRequestTest
assert_equal "::1", request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329"
- assert_equal nil, request.remote_ip
+ assert_nil request.remote_ip
request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334"
assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip
@@ -345,7 +345,7 @@ class RequestPort < BaseRequestTest
test "optional port" do
request = stub_request "HTTP_HOST" => "www.example.org:80"
- assert_equal nil, request.optional_port
+ assert_nil request.optional_port
request = stub_request "HTTP_HOST" => "www.example.org:8080"
assert_equal 8080, request.optional_port
@@ -537,7 +537,7 @@ class RequestCGI < BaseRequestTest
assert_equal "Basic", request.auth_type
assert_equal 0, request.content_length
- assert_equal nil, request.content_mime_type
+ assert_nil request.content_mime_type
assert_equal "CGI/1.1", request.gateway_interface
assert_equal "*/*", request.accept
assert_equal "UTF-8", request.accept_charset
@@ -760,8 +760,8 @@ class RequestMethod < BaseRequestTest
end
test "post uneffected by local inflections" do
- existing_acrnoyms = ActiveSupport::Inflector.inflections.acronyms.dup
- existing_acrnoym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup
+ existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup
+ existing_acronym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup
begin
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym "POS"
@@ -774,8 +774,8 @@ class RequestMethod < BaseRequestTest
ensure
# Reset original acronym set
ActiveSupport::Inflector.inflections do |inflect|
- inflect.send(:instance_variable_set, "@acronyms", existing_acrnoyms)
- inflect.send(:instance_variable_set, "@acronym_regex", existing_acrnoym_regex)
+ inflect.send(:instance_variable_set, "@acronyms", existing_acronyms)
+ inflect.send(:instance_variable_set, "@acronym_regex", existing_acronym_regex)
end
end
end
@@ -957,7 +957,7 @@ class RequestMimeType < BaseRequestTest
end
test "no content type" do
- assert_equal nil, stub_request.content_mime_type
+ assert_nil stub_request.content_mime_type
end
test "content type is XML" do
@@ -978,7 +978,7 @@ class RequestMimeType < BaseRequestTest
"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"
)
- assert_equal nil, request.negotiate_mime([Mime[:xml], Mime[:json]])
+ assert_nil request.negotiate_mime([Mime[:xml], Mime[:json]])
assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime[:html]])
assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime::ALL])
end
@@ -1192,7 +1192,7 @@ class RequestEtag < BaseRequestTest
test "doesn't match absent If-None-Match" do
request = stub_request
- assert_equal nil, request.if_none_match
+ assert_nil request.if_none_match
assert_equal [], request.if_none_match_etags
assert_not request.etag_matches?("foo")
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 400af42bac..7433c5ce0c 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -74,7 +74,7 @@ class ResponseTest < ActiveSupport::TestCase
@response.body = "Hello, World!"
# even though there's no explicitly set content-type,
- assert_equal nil, @response.content_type
+ assert_nil @response.content_type
# after the action reads back @response.body,
assert_equal "Hello, World!", @response.body
@@ -110,6 +110,11 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal "application/aaron", @response.content_type
end
+ def test_empty_content_type_returns_nil
+ @response.headers["Content-Type"] = ""
+ assert_nil @response.content_type
+ end
+
test "simple output" do
@response.body = "Hello, World!"
diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb
new file mode 100644
index 0000000000..cb5ca5888b
--- /dev/null
+++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb
@@ -0,0 +1,325 @@
+require "abstract_unit"
+
+class TestCustomUrlHelpers < ActionDispatch::IntegrationTest
+ class Linkable
+ attr_reader :id
+
+ def self.name
+ super.demodulize
+ end
+
+ def initialize(id)
+ @id = id
+ end
+
+ def linkable_type
+ self.class.name.underscore
+ end
+ end
+
+ class Category < Linkable; end
+ class Collection < Linkable; end
+ class Product < Linkable; end
+ class Manufacturer < Linkable; end
+
+ class Model
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+
+ def initialize(id = nil)
+ @id = id
+ end
+
+ remove_method :model_name
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, self.class.name.demodulize)
+ end
+
+ def persisted?
+ false
+ end
+ end
+
+ class Basket < Model; end
+ class User < Model; end
+ class Video < Model; end
+
+ class Article
+ attr_reader :id
+
+ def self.name
+ "Article"
+ end
+
+ def initialize(id)
+ @id = id
+ end
+ end
+
+ class Page
+ attr_reader :id
+
+ def self.name
+ super.demodulize
+ end
+
+ def initialize(id)
+ @id = id
+ end
+ end
+
+ class CategoryPage < Page; end
+ class ProductPage < Page; end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ default_url_options host: "www.example.com"
+
+ root to: "pages#index"
+ get "/basket", to: "basket#show", as: :basket
+ get "/posts/:id", to: "posts#show", as: :post
+ get "/profile", to: "users#profile", as: :profile
+ get "/media/:id", to: "media#show", as: :media
+ get "/pages/:id", to: "pages#show", as: :page
+
+ resources :categories, :collections, :products, :manufacturers
+
+ namespace :admin do
+ get "/dashboard", to: "dashboard#index"
+ end
+
+ direct(:website) { "http://www.rubyonrails.org" }
+ direct("string") { "http://www.rubyonrails.org" }
+ direct(:helper) { basket_url }
+ direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] }
+ direct(:nested) { |linkable| route_for(:linkable, linkable) }
+ direct(:params) { |params| params }
+ direct(:symbol) { :basket }
+ direct(:hash) { { controller: "basket", action: "show" } }
+ direct(:array) { [:admin, :dashboard] }
+ direct(:options) { |options| [:products, options] }
+ direct(:defaults, size: 10) { |options| [:products, options] }
+
+ direct(:browse, page: 1, size: 10) do |options|
+ [:products, options.merge(params.permit(:page, :size).to_h.symbolize_keys)]
+ end
+
+ resolve("Article") { |article| [:post, { id: article.id }] }
+ resolve("Basket") { |basket| [:basket] }
+ resolve("Manufacturer") { |manufacturer| route_for(:linkable, manufacturer) }
+ resolve("User", anchor: "details") { |user, options| [:profile, options] }
+ resolve("Video") { |video| [:media, { id: video.id }] }
+ resolve(%w[Page CategoryPage ProductPage]) { |page| [:page, { id: page.id }] }
+ end
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ include Routes.url_helpers
+
+ def setup
+ @category = Category.new("1")
+ @collection = Collection.new("2")
+ @product = Product.new("3")
+ @manufacturer = Manufacturer.new("apple")
+ @basket = Basket.new
+ @user = User.new
+ @video = Video.new("4")
+ @article = Article.new("5")
+ @page = Page.new("6")
+ @category_page = CategoryPage.new("7")
+ @product_page = ProductPage.new("8")
+ @path_params = { "controller" => "pages", "action" => "index" }
+ @unsafe_params = ActionController::Parameters.new(@path_params)
+ @safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action)
+ end
+
+ def params
+ ActionController::Parameters.new(page: 2, size: 25)
+ end
+
+ def test_direct_paths
+ assert_equal "/", website_path
+ assert_equal "/", Routes.url_helpers.website_path
+
+ assert_equal "/", string_path
+ assert_equal "/", Routes.url_helpers.string_path
+
+ assert_equal "/basket", helper_path
+ assert_equal "/basket", Routes.url_helpers.helper_path
+
+ assert_equal "/categories/1", linkable_path(@category)
+ assert_equal "/categories/1", Routes.url_helpers.linkable_path(@category)
+ assert_equal "/collections/2", linkable_path(@collection)
+ assert_equal "/collections/2", Routes.url_helpers.linkable_path(@collection)
+ assert_equal "/products/3", linkable_path(@product)
+ assert_equal "/products/3", Routes.url_helpers.linkable_path(@product)
+
+ assert_equal "/categories/1", nested_path(@category)
+ assert_equal "/categories/1", Routes.url_helpers.nested_path(@category)
+
+ assert_equal "/", params_path(@safe_params)
+ assert_equal "/", Routes.url_helpers.params_path(@safe_params)
+ assert_raises(ArgumentError) { params_path(@unsafe_params) }
+ assert_raises(ArgumentError) { Routes.url_helpers.params_path(@unsafe_params) }
+
+ assert_equal "/basket", symbol_path
+ assert_equal "/basket", Routes.url_helpers.symbol_path
+ assert_equal "/basket", hash_path
+ assert_equal "/basket", Routes.url_helpers.hash_path
+ assert_equal "/admin/dashboard", array_path
+ assert_equal "/admin/dashboard", Routes.url_helpers.array_path
+
+ assert_equal "/products?page=2", options_path(page: 2)
+ assert_equal "/products?page=2", Routes.url_helpers.options_path(page: 2)
+ assert_equal "/products?size=10", defaults_path
+ assert_equal "/products?size=10", Routes.url_helpers.defaults_path
+ assert_equal "/products?size=20", defaults_path(size: 20)
+ assert_equal "/products?size=20", Routes.url_helpers.defaults_path(size: 20)
+
+ assert_equal "/products?page=2&size=25", browse_path
+ assert_raises(NameError) { Routes.url_helpers.browse_path }
+ end
+
+ def test_direct_urls
+ assert_equal "http://www.rubyonrails.org", website_url
+ assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_url
+
+ assert_equal "http://www.rubyonrails.org", string_url
+ assert_equal "http://www.rubyonrails.org", Routes.url_helpers.string_url
+
+ assert_equal "http://www.example.com/basket", helper_url
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.helper_url
+
+ assert_equal "http://www.example.com/categories/1", linkable_url(@category)
+ assert_equal "http://www.example.com/categories/1", Routes.url_helpers.linkable_url(@category)
+ assert_equal "http://www.example.com/collections/2", linkable_url(@collection)
+ assert_equal "http://www.example.com/collections/2", Routes.url_helpers.linkable_url(@collection)
+ assert_equal "http://www.example.com/products/3", linkable_url(@product)
+ assert_equal "http://www.example.com/products/3", Routes.url_helpers.linkable_url(@product)
+
+ assert_equal "http://www.example.com/categories/1", nested_url(@category)
+ assert_equal "http://www.example.com/categories/1", Routes.url_helpers.nested_url(@category)
+
+ assert_equal "http://www.example.com/", params_url(@safe_params)
+ assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params)
+ assert_raises(ArgumentError) { params_url(@unsafe_params) }
+ assert_raises(ArgumentError) { Routes.url_helpers.params_url(@unsafe_params) }
+
+ assert_equal "http://www.example.com/basket", symbol_url
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url
+ assert_equal "http://www.example.com/basket", hash_url
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.hash_url
+ assert_equal "http://www.example.com/admin/dashboard", array_url
+ assert_equal "http://www.example.com/admin/dashboard", Routes.url_helpers.array_url
+
+ assert_equal "http://www.example.com/products?page=2", options_url(page: 2)
+ assert_equal "http://www.example.com/products?page=2", Routes.url_helpers.options_url(page: 2)
+ assert_equal "http://www.example.com/products?size=10", defaults_url
+ assert_equal "http://www.example.com/products?size=10", Routes.url_helpers.defaults_url
+ assert_equal "http://www.example.com/products?size=20", defaults_url(size: 20)
+ assert_equal "http://www.example.com/products?size=20", Routes.url_helpers.defaults_url(size: 20)
+
+ assert_equal "http://www.example.com/products?page=2&size=25", browse_url
+ assert_raises(NameError) { Routes.url_helpers.browse_url }
+ end
+
+ def test_resolve_paths
+ assert_equal "/basket", polymorphic_path(@basket)
+ assert_equal "/basket", Routes.url_helpers.polymorphic_path(@basket)
+
+ assert_equal "/profile#details", polymorphic_path(@user)
+ assert_equal "/profile#details", Routes.url_helpers.polymorphic_path(@user)
+
+ assert_equal "/profile#password", polymorphic_path(@user, anchor: "password")
+ assert_equal "/profile#password", Routes.url_helpers.polymorphic_path(@user, anchor: "password")
+
+ assert_equal "/media/4", polymorphic_path(@video)
+ assert_equal "/media/4", Routes.url_helpers.polymorphic_path(@video)
+ assert_equal "/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @video)
+
+ assert_equal "/posts/5", polymorphic_path(@article)
+ assert_equal "/posts/5", Routes.url_helpers.polymorphic_path(@article)
+ assert_equal "/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @article)
+
+ assert_equal "/pages/6", polymorphic_path(@page)
+ assert_equal "/pages/6", Routes.url_helpers.polymorphic_path(@page)
+ assert_equal "/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @page)
+
+ assert_equal "/pages/7", polymorphic_path(@category_page)
+ assert_equal "/pages/7", Routes.url_helpers.polymorphic_path(@category_page)
+ assert_equal "/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @category_page)
+
+ assert_equal "/pages/8", polymorphic_path(@product_page)
+ assert_equal "/pages/8", Routes.url_helpers.polymorphic_path(@product_page)
+ assert_equal "/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @product_page)
+
+ assert_equal "/manufacturers/apple", polymorphic_path(@manufacturer)
+ assert_equal "/manufacturers/apple", Routes.url_helpers.polymorphic_path(@manufacturer)
+ end
+
+ def test_resolve_urls
+ assert_equal "http://www.example.com/basket", polymorphic_url(@basket)
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket)
+ assert_equal "http://www.example.com/basket", polymorphic_url(@basket)
+ assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket)
+
+ assert_equal "http://www.example.com/profile#details", polymorphic_url(@user)
+ assert_equal "http://www.example.com/profile#details", Routes.url_helpers.polymorphic_url(@user)
+
+ assert_equal "http://www.example.com/profile#password", polymorphic_url(@user, anchor: "password")
+ assert_equal "http://www.example.com/profile#password", Routes.url_helpers.polymorphic_url(@user, anchor: "password")
+
+ assert_equal "http://www.example.com/media/4", polymorphic_url(@video)
+ assert_equal "http://www.example.com/media/4", Routes.url_helpers.polymorphic_url(@video)
+ assert_equal "http://www.example.com/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @video)
+
+ assert_equal "http://www.example.com/posts/5", polymorphic_url(@article)
+ assert_equal "http://www.example.com/posts/5", Routes.url_helpers.polymorphic_url(@article)
+ assert_equal "http://www.example.com/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @article)
+
+ assert_equal "http://www.example.com/pages/6", polymorphic_url(@page)
+ assert_equal "http://www.example.com/pages/6", Routes.url_helpers.polymorphic_url(@page)
+ assert_equal "http://www.example.com/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @page)
+
+ assert_equal "http://www.example.com/pages/7", polymorphic_url(@category_page)
+ assert_equal "http://www.example.com/pages/7", Routes.url_helpers.polymorphic_url(@category_page)
+ assert_equal "http://www.example.com/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @category_page)
+
+ assert_equal "http://www.example.com/pages/8", polymorphic_url(@product_page)
+ assert_equal "http://www.example.com/pages/8", Routes.url_helpers.polymorphic_url(@product_page)
+ assert_equal "http://www.example.com/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @product_page)
+
+ assert_equal "http://www.example.com/manufacturers/apple", polymorphic_url(@manufacturer)
+ assert_equal "http://www.example.com/manufacturers/apple", Routes.url_helpers.polymorphic_url(@manufacturer)
+ end
+
+ def test_defining_direct_inside_a_scope_raises_runtime_error
+ routes = ActionDispatch::Routing::RouteSet.new
+
+ assert_raises RuntimeError do
+ routes.draw do
+ namespace :admin do
+ direct(:rubyonrails) { "http://www.rubyonrails.org" }
+ end
+ end
+ end
+ end
+
+ def test_defining_resolve_inside_a_scope_raises_runtime_error
+ routes = ActionDispatch::Routing::RouteSet.new
+
+ assert_raises RuntimeError do
+ routes.draw do
+ namespace :admin do
+ resolve("User") { "/profile" }
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
index 4987ed84e4..179aee9ba7 100644
--- a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
+++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
@@ -7,7 +7,7 @@ class IPv6IntegrationTest < ActionDispatch::IntegrationTest
class ::BadRouteRequestController < ActionController::Base
include Routes.url_helpers
def index
- render text: foo_path
+ render plain: foo_path
end
def foo
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 92d323f292..64818e6ca1 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -1603,7 +1603,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "account/login", to: redirect("/login")
end
- previous_host, self.host = self.host, "www.example.com:3000"
+ previous_host, self.host = host, "www.example.com:3000"
get "/account/login"
verify_redirect "http://www.example.com:3000/login"
@@ -4189,7 +4189,7 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest
test "parameters are reset between constraint checks" do
get "/bar"
- assert_equal nil, @request.params[:foo]
+ assert_nil @request.params[:foo]
assert_equal "bar", @request.params[:bar]
end
end
@@ -4913,3 +4913,113 @@ class TestInternalRoutingParams < ActionDispatch::IntegrationTest
)
end
end
+
+class FlashRedirectTest < ActionDispatch::IntegrationTest
+ SessionKey = "_myapp_session"
+ Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+
+ class KeyGeneratorMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.key_generator"] ||= Generator
+ @app.call(env)
+ end
+ end
+
+ class FooController < ActionController::Base
+ def bar
+ render plain: (flash[:foo] || "foo")
+ end
+ end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ get "/foo", to: redirect { |params, req| req.flash[:foo] = "bar"; "/bar" }
+ get "/bar", to: "flash_redirect_test/foo#bar"
+ end
+
+ APP = build_app Routes do |middleware|
+ middleware.use KeyGeneratorMiddleware
+ middleware.use ActionDispatch::Session::CookieStore, key: SessionKey
+ middleware.use ActionDispatch::Flash
+ middleware.delete ActionDispatch::ShowExceptions
+ end
+
+ def app
+ APP
+ end
+
+ include Routes.url_helpers
+
+ def test_block_redirect_commits_flash
+ get "/foo", env: { "action_dispatch.key_generator" => Generator }
+ assert_response :redirect
+
+ follow_redirect!
+ assert_equal "bar", response.body
+ end
+end
+
+class TestRecognizePath < ActionDispatch::IntegrationTest
+ class PageConstraint
+ attr_reader :key, :pattern
+
+ def initialize(key, pattern)
+ @key = key
+ @pattern = pattern
+ end
+
+ def matches?(request)
+ request.path_parameters[key] =~ pattern
+ end
+ end
+
+ stub_controllers do |routes|
+ Routes = routes
+ routes.draw do
+ get "/hash/:foo", to: "pages#show", constraints: { foo: /foo/ }
+ get "/hash/:bar", to: "pages#show", constraints: { bar: /bar/ }
+
+ get "/proc/:foo", to: "pages#show", constraints: proc { |r| r.path_parameters[:foo] =~ /foo/ }
+ get "/proc/:bar", to: "pages#show", constraints: proc { |r| r.path_parameters[:bar] =~ /bar/ }
+
+ get "/class/:foo", to: "pages#show", constraints: PageConstraint.new(:foo, /foo/)
+ get "/class/:bar", to: "pages#show", constraints: PageConstraint.new(:bar, /bar/)
+ end
+ end
+
+ APP = build_app Routes
+ def app
+ APP
+ end
+
+ def test_hash_constraints_dont_leak_between_routes
+ expected_params = { controller: "pages", action: "show", bar: "bar" }
+ actual_params = recognize_path("/hash/bar")
+
+ assert_equal expected_params, actual_params
+ end
+
+ def test_proc_constraints_dont_leak_between_routes
+ expected_params = { controller: "pages", action: "show", bar: "bar" }
+ actual_params = recognize_path("/proc/bar")
+
+ assert_equal expected_params, actual_params
+ end
+
+ def test_class_constraints_dont_leak_between_routes
+ expected_params = { controller: "pages", action: "show", bar: "bar" }
+ actual_params = recognize_path("/class/bar")
+
+ assert_equal expected_params, actual_params
+ end
+
+ private
+
+ def recognize_path(*args)
+ Routes.recognize_path(*args)
+ end
+end
diff --git a/actionpack/test/dispatch/runner_test.rb b/actionpack/test/dispatch/runner_test.rb
index 969933c9ed..b76bf4a320 100644
--- a/actionpack/test/dispatch/runner_test.rb
+++ b/actionpack/test/dispatch/runner_test.rb
@@ -4,7 +4,6 @@ class RunnerTest < ActiveSupport::TestCase
test "runner preserves the setting of integration_session" do
runner = Class.new do
def before_setup
-
end
end.new
diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb
index a60629a7ee..859059063f 100644
--- a/actionpack/test/dispatch/session/cache_store_test.rb
+++ b/actionpack/test/dispatch/session/cache_store_test.rb
@@ -142,20 +142,20 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
get "/get_session_value"
assert_response :success
- assert_equal nil, headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists"
+ assert_nil headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists"
end
end
def test_prevents_session_fixation
with_test_route_set do
- assert_equal nil, @cache.read("_session_id:0xhax")
+ assert_nil @cache.read("_session_id:0xhax")
cookies["_session_id"] = "0xhax"
get "/set_session_value"
assert_response :success
assert_not_equal "0xhax", cookies["_session_id"]
- assert_equal nil, @cache.read("_session_id:0xhax")
+ assert_nil @cache.read("_session_id:0xhax")
assert_equal({ "foo" => "bar" }, @cache.read("_session_id:#{cookies['_session_id']}"))
end
end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 013d289c6d..63dfc07c0d 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -108,7 +108,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set(secure: true) do
get "/set_session_value"
assert_response :success
- assert_equal nil, headers["Set-Cookie"]
+ assert_nil headers["Set-Cookie"]
end
end
@@ -169,17 +169,17 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set do
get "/no_session_access"
assert_response :success
- assert_equal nil, headers["Set-Cookie"]
+ assert_nil headers["Set-Cookie"]
end
end
def test_doesnt_write_session_cookie_if_session_is_unchanged
with_test_route_set do
- cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" +
+ cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" \
"fef868465920f415f2c0652d6910d3af288a0367"
get "/no_session_access"
assert_response :success
- assert_equal nil, headers["Set-Cookie"]
+ assert_nil headers["Set-Cookie"]
end
end
diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb
index c2d0719b4e..121e9ebef7 100644
--- a/actionpack/test/dispatch/session/mem_cache_store_test.rb
+++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb
@@ -157,7 +157,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
get "/get_session_value"
assert_response :success
- assert_equal nil, headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists"
+ assert_nil headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists"
end
rescue Dalli::RingError => ex
skip ex.message, ex.backtrace
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index e29ffa750c..757e26973f 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -102,7 +102,11 @@ class StrictTransportSecurityTest < SSLTest
def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {})
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
get url
- assert_equal expected, response.headers["Strict-Transport-Security"]
+ if expected.nil?
+ assert_nil response.headers["Strict-Transport-Security"]
+ else
+ assert_equal expected, response.headers["Strict-Transport-Security"]
+ end
end
test "enabled by default" do
@@ -130,7 +134,7 @@ class StrictTransportSecurityTest < SSLTest
end
test ":expires supports AS::Duration arguments" do
- assert_hsts "max-age=31557600; includeSubDomains", hsts: { expires: 1.year }
+ assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year }
end
test "include subdomains" do
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index cdb905f298..3082d1072b 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -163,7 +163,7 @@ module StaticTests
assert_equal file_name, env["PATH_INFO"]
end
- def test_serves_gzip_with_propper_content_type_fallback
+ def test_serves_gzip_with_proper_content_type_fallback
file_name = "/gzip/foo.zoo"
response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip")
assert_gzip file_name, response
@@ -177,9 +177,9 @@ module StaticTests
last_modified = File.mtime(File.join(@root, "#{file_name}.gz"))
response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip", "HTTP_IF_MODIFIED_SINCE" => last_modified.httpdate)
assert_equal 304, response.status
- assert_equal nil, response.headers["Content-Type"]
- assert_equal nil, response.headers["Content-Encoding"]
- assert_equal nil, response.headers["Vary"]
+ assert_nil response.headers["Content-Type"]
+ assert_nil response.headers["Content-Encoding"]
+ assert_nil response.headers["Vary"]
end
def test_serves_files_with_headers
@@ -204,7 +204,7 @@ module StaticTests
end
# Windows doesn't allow \ / : * ? " < > | in filenames
- unless RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
+ unless Gem.win_platform?
def test_serves_static_file_with_colon
with_static_file "/foo/foo:bar.html" do |file|
assert_html file, get("/foo/foo%3Abar.html")
@@ -224,7 +224,7 @@ module StaticTests
def assert_gzip(file_name, response)
expected = File.read("#{FIXTURE_LOAD_PATH}/#{public_path}" + file_name)
- actual = Zlib::GzipReader.new(StringIO.new(response.body)).read
+ actual = ActiveSupport::Gzip.decompress(response.body)
assert_equal expected, actual
end
diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb
new file mode 100644
index 0000000000..814e1d707b
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -0,0 +1,21 @@
+require "abstract_unit"
+require "action_dispatch/system_testing/driver"
+
+class DriverTest < ActiveSupport::TestCase
+ test "initializing the driver" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium)
+ assert_equal :selenium, driver.instance_variable_get(:@name)
+ end
+
+ 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 [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ test "selenium? returns false if driver is poltergeist" do
+ assert_not ActionDispatch::SystemTesting::Driver.new(:poltergeist).send(:selenium?)
+ end
+end
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
new file mode 100644
index 0000000000..a83818fd80
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -0,0 +1,41 @@
+require "abstract_unit"
+require "action_dispatch/system_testing/test_helpers/screenshot_helper"
+require "capybara/dsl"
+
+class ScreenshotHelperTest < ActiveSupport::TestCase
+ test "image path is saved in tmp directory" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ assert_equal "tmp/screenshots/x.png", new_test.send(:image_path)
+ end
+
+ test "image path includes failures text if test did not pass" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ new_test.stub :passed?, false do
+ assert_equal "tmp/screenshots/failures_x.png", new_test.send(:image_path)
+ end
+ end
+
+ test "image path does not include failures text if test skipped" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+
+ new_test.stub :passed?, false do
+ new_test.stub :skipped?, true do
+ assert_equal "tmp/screenshots/x.png", new_test.send(:image_path)
+ end
+ end
+ end
+end
+
+class RackTestScreenshotsTest < DrivenByRackTest
+ test "rack_test driver does not support screenshot" do
+ assert_not self.send(:supports_screenshot?)
+ end
+end
+
+class SeleniumScreenshotsTest < DrivenBySeleniumWithChrome
+ test "selenium driver supports screenshot" do
+ assert self.send(:supports_screenshot?)
+ end
+end
diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb
new file mode 100644
index 0000000000..10412d6367
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/server_test.rb
@@ -0,0 +1,17 @@
+require "abstract_unit"
+require "capybara/dsl"
+require "action_dispatch/system_testing/server"
+
+class ServerTest < ActiveSupport::TestCase
+ setup do
+ ActionDispatch::SystemTesting::Server.new.run
+ end
+
+ test "initializing the server port" do
+ assert_includes Capybara.servers, :rails_puma
+ end
+
+ test "port is always included" do
+ assert Capybara.always_include_port, "expected Capybara.always_include_port to be true"
+ 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
new file mode 100644
index 0000000000..1a9421c098
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -0,0 +1,13 @@
+require "abstract_unit"
+
+class SetDriverToRackTestTest < DrivenByRackTest
+ test "uses rack_test" do
+ assert_equal :rack_test, Capybara.current_driver
+ end
+end
+
+class SetDriverToSeleniumTest < DrivenBySeleniumWithChrome
+ test "uses selenium" do
+ assert_equal :selenium, Capybara.current_driver
+ end
+end
diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb
index b479af781d..85a6df4975 100644
--- a/actionpack/test/dispatch/test_request_test.rb
+++ b/actionpack/test/dispatch/test_request_test.rb
@@ -30,7 +30,7 @@ class TestRequestTest < ActiveSupport::TestCase
req = ActionDispatch::TestRequest.create({})
assert_equal({}, req.cookies)
- assert_equal nil, req.env["HTTP_COOKIE"]
+ assert_nil req.env["HTTP_COOKIE"]
req.cookie_jar["user_name"] = "david"
assert_cookies({ "user_name" => "david" }, req.cookie_jar)
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index d61a8c023a..2c74617944 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -20,7 +20,7 @@ module ActionDispatch
"/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar\Z},
"/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))\Z},
}.each do |path, expected|
- define_method(:"test_to_regexp_#{path}") do
+ define_method(:"test_to_regexp_#{Regexp.escape(path)}") do
path = Pattern.build(
path,
{ controller: /.+/ },
@@ -44,7 +44,7 @@ module ActionDispatch
"/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar},
"/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))},
}.each do |path, expected|
- define_method(:"test_to_non_anchored_regexp_#{path}") do
+ define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do
path = Pattern.build(
path,
{ controller: /.+/ },
@@ -67,7 +67,7 @@ module ActionDispatch
"/:controller/*foo" => %w{ controller foo },
"/:controller/*foo/bar" => %w{ controller foo },
}.each do |path, expected|
- define_method(:"test_names_#{path}") do
+ define_method(:"test_names_#{Regexp.escape(path)}") do
path = Pattern.build(
path,
{ controller: /.+/ },
diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb
index d2a8163ffb..8fd73970b8 100644
--- a/actionpack/test/journey/route_test.rb
+++ b/actionpack/test/journey/route_test.rb
@@ -96,7 +96,7 @@ module ActionDispatch
path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)"
generic = Route.build "name", nil, path, constraints, [], {}
- knowledge = { id: 20, controller: "pages", action: "show" }
+ knowledge = { "id" => true, "controller" => true, "action" => true }
routes = [specific, generic]
diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb
index 046b4167bb..b768553e7a 100644
--- a/actionpack/test/lib/controller/fake_models.rb
+++ b/actionpack/test/lib/controller/fake_models.rb
@@ -1,6 +1,6 @@
require "active_model"
-class Customer < Struct.new(:name, :id)
+Customer = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -28,7 +28,7 @@ class Customer < Struct.new(:name, :id)
end
end
-class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost)
+Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do
extend ActiveModel::Naming
include ActiveModel::Conversion
extend ActiveModel::Translation
diff --git a/actionview/.gitignore b/actionview/.gitignore
new file mode 100644
index 0000000000..0a04b29786
--- /dev/null
+++ b/actionview/.gitignore
@@ -0,0 +1,2 @@
+/lib/assets/compiled
+/tmp
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 6e6ce64e72..c514e757c8 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,120 +1 @@
-* Removed deprecated `#original_exception` in `ActionView::Template::Error`.
-
- *Rafael Mendonça França*
-
-* Render now accepts any keys for locals, including reserved keywords.
-
- Only locals with valid variable names get set directly. Others
- will still be available in `local_assigns`.
-
- Example of render with reserved keywords:
-
- ```erb
- <%= render "example", class: "text-center", message: "Hello world!" %>
-
- <!-- _example.html.erb: -->
- <%= tag.div class: local_assigns[:class] do %>
- <p><%= message %></p>
- <% end %>
- ```
-
- *Peter Schilling*, *Matthew Draper*
-
-* Show cache hits and misses when rendering partials.
-
- Partials using the `cache` helper will show whether a render hit or missed
- the cache:
-
- ```
- Rendered messages/_message.html.erb in 1.2 ms [cache hit]
- Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss]
- ```
-
- This removes the need for the old fragment cache logging:
-
- ```
- Read fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/d0bdf2974e1ef6d31685c3b392ad0b74 (0.6ms)
- Rendered messages/_message.html.erb in 1.2 ms [cache hit]
- Write fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/3b4e249ac9d168c617e32e84b99218b5 (1.1ms)
- Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss]
- ```
-
- Though that full output can be reenabled with
- `config.action_controller.enable_fragment_cache_logging = true`.
-
- *Stan Lo*
-
-* Changed partial rendering with a collection to allow collections which
- implement `to_a`.
-
- Extracting the collection option had an optimization to avoid unnecessary
- queries of ActiveRecord Relations by calling `#to_ary` on the given
- collection. Instances of `Enumerator` or `Enumerable` are valid
- collections, but they do not implement `#to_ary`. By changing this to
- `#to_a`, they will now be extracted and rendered as expected.
-
- *Steven Harman*
-
-* New syntax for tag helpers. Avoid positional parameters and support HTML5 by default.
- Example usage of tag helpers before:
-
- ```ruby
- tag(:br, nil, true)
- content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
-
- <%= content_tag :div, class: "strong" do -%>
- Hello world!
- <% end -%>
- ```
-
- Example usage of tag helpers after:
-
- ```ruby
- tag.br
- tag.div tag.p("Hello world!"), class: "strong"
-
- <%= tag.div class: "strong" do %>
- Hello world!
- <% end %>
- ```
-
- *Marek Kirejczyk*, *Kasper Timm Hansen*
-
-* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` fields.
-
- As a new specification of the HTML 5 the text field type `datetime` will no longer exist
- and it is recommended to use `datetime-local`.
- Ref: https://html.spec.whatwg.org/multipage/forms.html#local-date-and-time-state-(type=datetime-local)
-
- *Herminio Torres*
-
-* Raw template handler (which is also the default template handler in Rails 5) now outputs
- HTML-safe strings.
-
- In Rails 5 the default template handler was changed to the raw template handler. Because
- the ERB template handler escaped strings by default this broke some applications that
- expected plain JS or HTML files to be rendered unescaped. This fixes the issue caused
- by changing the default handler by changing the Raw template handler to output HTML-safe
- strings.
-
- *Eileen M. Uchitelle*
-
-* `select_tag`'s `include_blank` option for generation for blank option tag, now adds an empty space label,
- when the value as well as content for option tag are empty, so that we conform with html specification.
- Ref: https://www.w3.org/TR/html5/forms.html#the-option-element.
-
- Generation of option before:
-
- ```html
- <option value=""></option>
- ```
-
- Generation of option after:
-
- ```html
- <option value="" label=" "></option>
- ```
-
- *Vipul A M*
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
index 8573eb1225..ac810e86d0 100644
--- a/actionview/MIT-LICENSE
+++ b/actionview/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2017 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/RUNNING_UJS_TESTS.rdoc b/actionview/RUNNING_UJS_TESTS.rdoc
new file mode 100644
index 0000000000..a575624a06
--- /dev/null
+++ b/actionview/RUNNING_UJS_TESTS.rdoc
@@ -0,0 +1,7 @@
+== 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.
+
+rake ujs:server
diff --git a/actionview/Rakefile b/actionview/Rakefile
index 6c3fc59b0a..00ab92129d 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -1,9 +1,10 @@
require "rake/testtask"
+require "fileutils"
desc "Default Task"
task default: :test
-task :package
+task package: "assets:compile"
# Run the unit tests
@@ -25,6 +26,32 @@ namespace :test do
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
+ 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")
+
+ start_time = Time.now
+
+ loop do
+ break if system("lsof -i :4567 >/dev/null")
+
+ if Time.now - start_time > 5
+ puts "Timed out after 5 seconds"
+ exit 1
+ end
+ end
+
+ system("npm run lint && phantomjs ../ci/phantomjs.js http://localhost:4567/")
+ status = $?.to_i
+ ensure
+ Process.kill("KILL", pid) if pid
+ FileUtils.rm_f("log")
+ end
+
+ exit status
+ end
+
namespace :integration do
desc "ActiveRecord Integration Tests"
Rake::TestTask.new(:active_record) do |t|
@@ -46,6 +73,21 @@ namespace :test do
end
end
+namespace :ujs do
+ desc "Starts the test server"
+ task :server do
+ system "bundle exec rackup test/ujs/config.ru -p 4567 -s puma"
+ end
+end
+
+namespace :assets do
+ desc "Compile Action View assets"
+ task :compile do
+ require "blade"
+ Blade.build
+ end
+end
+
task :lines do
load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics"
files = FileList["lib/**/*.rb"]
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index 7bfdfbe29a..cfaa5007a1 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -22,8 +22,8 @@ Gem::Specification.new do |s|
s.add_dependency "activesupport", version
s.add_dependency "builder", "~> 3.1"
- s.add_dependency "erubis", "~> 2.7.0"
- s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.2"
+ s.add_dependency "erubi", "~> 1.4"
+ s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.3"
s.add_dependency "rails-dom-testing", "~> 2.0"
s.add_development_dependency "actionpack", version
diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE
new file mode 100644
index 0000000000..befcbdc7b7
--- /dev/null
+++ b/actionview/app/assets/javascripts/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2007-2017 Rails Core team
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md
new file mode 100644
index 0000000000..92f3e8a3b3
--- /dev/null
+++ b/actionview/app/assets/javascripts/README.md
@@ -0,0 +1,49 @@
+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:
+
+- force confirmation dialogs for various actions;
+- make non-GET requests from hyperlinks;
+- 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.
+
+Requirements
+------------
+
+- HTML5 doctype (optional).
+
+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 using npm
+------------
+
+Run `npm install rails-ujs --save` to install the rails-ujs package.
+
+Installation using Yarn
+------------
+
+Run `yarn add rails-ujs` to install the rails-ujs package.
+
+Usage
+------------
+
+Require `rails-ujs` into your application.js manifest.
+
+```javascript
+//= require rails-ujs
+```
+
+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"
+[validator]: http://validator.w3.org/
+[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html
diff --git a/actionview/app/assets/javascripts/config.coffee b/actionview/app/assets/javascripts/config.coffee
new file mode 100644
index 0000000000..a93325e903
--- /dev/null
+++ b/actionview/app/assets/javascripts/config.coffee
@@ -0,0 +1,34 @@
+#= export Rails
+
+@Rails =
+ # Link elements bound by rails-ujs
+ linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]'
+
+ # Button elements bound by rails-ujs
+ buttonClickSelector:
+ selector: 'button[data-remote]:not([form]), button[data-confirm]:not([form])'
+ exclude: 'form button'
+
+ # Select elements bound by rails-ujs
+ inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]'
+
+ # Form elements bound by rails-ujs
+ formSubmitSelector: 'form'
+
+ # Form input elements bound by rails-ujs
+ formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])'
+
+ # Form input elements disabled during form submission
+ formDisableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled'
+
+ # Form input elements re-enabled after form submission
+ formEnableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled'
+
+ # Form file input elements
+ fileInputSelector: 'input[name][type=file]:not([disabled])'
+
+ # Link onClick disable selector with possible reenable after remote submission
+ linkDisableSelector: 'a[data-disable-with], a[data-disable]'
+
+ # Button onClick disable selector with possible reenable after remote submission
+ buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]'
diff --git a/actionview/app/assets/javascripts/features/confirm.coffee b/actionview/app/assets/javascripts/features/confirm.coffee
new file mode 100644
index 0000000000..72b5aaa218
--- /dev/null
+++ b/actionview/app/assets/javascripts/features/confirm.coffee
@@ -0,0 +1,26 @@
+#= require_tree ../utils
+
+{ fire, stopEverything } = Rails
+
+Rails.handleConfirm = (e) ->
+ stopEverything(e) unless allowAction(this)
+
+# For 'data-confirm' attribute:
+# - Fires `confirm` event
+# - Shows the confirmation dialog
+# - Fires the `confirm:complete` event
+#
+# Returns `true` if no function stops the chain and user chose yes `false` otherwise.
+# Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
+# Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
+# return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
+allowAction = (element) ->
+ message = element.getAttribute('data-confirm')
+ return true unless message
+
+ answer = false
+ if fire(element, 'confirm')
+ try answer = confirm(message)
+ callback = fire(element, 'confirm:complete', [answer])
+
+ answer and callback
diff --git a/actionview/app/assets/javascripts/features/disable.coffee b/actionview/app/assets/javascripts/features/disable.coffee
new file mode 100644
index 0000000000..90aa3bdf0e
--- /dev/null
+++ b/actionview/app/assets/javascripts/features/disable.coffee
@@ -0,0 +1,82 @@
+#= require_tree ../utils
+
+{ matches, getData, setData, stopEverything, formElements } = Rails
+
+Rails.handleDisabledElement = (e) ->
+ element = this
+ stopEverything(e) if element.disabled
+
+# Unified function to enable an element (link, button and form)
+Rails.enableElement = (e) ->
+ element = if e instanceof Event then e.target else e
+ if matches(element, Rails.linkDisableSelector)
+ enableLinkElement(element)
+ else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formEnableSelector)
+ enableFormElement(element)
+ else if matches(element, Rails.formSubmitSelector)
+ enableFormElements(element)
+
+# Unified function to disable an element (link, button and form)
+Rails.disableElement = (e) ->
+ element = if e instanceof Event then e.target else e
+ if matches(element, Rails.linkDisableSelector)
+ disableLinkElement(element)
+ else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formDisableSelector)
+ disableFormElement(element)
+ else if matches(element, Rails.formSubmitSelector)
+ disableFormElements(element)
+
+# Replace element's html with the 'data-disable-with' after storing original html
+# and prevent clicking on it
+disableLinkElement = (element) ->
+ replacement = element.getAttribute('data-disable-with')
+ if replacement?
+ setData(element, 'ujs:enable-with', element.innerHTML) # store enabled state
+ element.innerHTML = replacement
+ element.addEventListener('click', stopEverything) # prevent further clicking
+ setData(element, 'ujs:disabled', true)
+
+# Restore element to its original state which was disabled by 'disableLinkElement' above
+enableLinkElement = (element) ->
+ originalText = getData(element, 'ujs:enable-with')
+ if originalText?
+ element.innerHTML = originalText # set to old enabled state
+ setData(element, 'ujs:enable-with', null) # clean up cache
+ element.removeEventListener('click', stopEverything) # enable element
+ setData(element, 'ujs:disabled', null)
+
+# Disables form elements:
+# - Caches element value in 'ujs:enable-with' data store
+# - Replaces element text with value of 'data-disable-with' attribute
+# - Sets disabled property to true
+disableFormElements = (form) ->
+ formElements(form, Rails.formDisableSelector).forEach(disableFormElement)
+
+disableFormElement = (element) ->
+ replacement = element.getAttribute('data-disable-with')
+ if replacement?
+ if matches(element, 'button')
+ setData(element, 'ujs:enable-with', element.innerHTML)
+ element.innerHTML = replacement
+ else
+ setData(element, 'ujs:enable-with', element.value)
+ element.value = replacement
+ element.disabled = true
+ setData(element, 'ujs:disabled', true)
+
+# Re-enables disabled form elements:
+# - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
+# - Sets disabled property to false
+enableFormElements = (form) ->
+ formElements(form, Rails.formEnableSelector).forEach(enableFormElement)
+
+enableFormElement = (element) ->
+ originalText = getData(element, 'ujs:enable-with')
+ if originalText?
+ if matches(element, 'button')
+ element.innerHTML = originalText
+ else
+ element.value = originalText
+ setData(element, 'ujs:enable-with', null) # clean up cache
+ element.disabled = false
+ setData(element, 'ujs:disabled', null)
diff --git a/actionview/app/assets/javascripts/features/method.coffee b/actionview/app/assets/javascripts/features/method.coffee
new file mode 100644
index 0000000000..d04d9414dd
--- /dev/null
+++ b/actionview/app/assets/javascripts/features/method.coffee
@@ -0,0 +1,34 @@
+#= require_tree ../utils
+
+{ stopEverything } = Rails
+
+# Handles "data-method" on links such as:
+# <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
+Rails.handleMethod = (e) ->
+ link = this
+ method = link.getAttribute('data-method')
+ return unless method
+
+ href = Rails.href(link)
+ csrfToken = Rails.csrfToken()
+ csrfParam = Rails.csrfParam()
+ form = document.createElement('form')
+ formContent = "<input name='_method' value='#{method}' type='hidden' />"
+
+ if csrfParam? and csrfToken? and not Rails.isCrossDomain(href)
+ formContent += "<input name='#{csrfParam}' value='#{csrfToken}' type='hidden' />"
+
+ # Must trigger submit by click on a button, else "submit" event handler won't work!
+ # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit
+ formContent += '<input type="submit" />'
+
+ form.method = 'post'
+ form.action = href
+ form.target = link.target
+ form.innerHTML = formContent
+ form.style.display = 'none'
+
+ document.body.appendChild(form)
+ form.querySelector('[type="submit"]').click()
+
+ stopEverything(e)
diff --git a/actionview/app/assets/javascripts/features/remote.coffee b/actionview/app/assets/javascripts/features/remote.coffee
new file mode 100644
index 0000000000..852587042c
--- /dev/null
+++ b/actionview/app/assets/javascripts/features/remote.coffee
@@ -0,0 +1,90 @@
+#= require_tree ../utils
+
+{
+ matches, getData, setData
+ fire, stopEverything
+ ajax, isCrossDomain
+ serializeElement
+} = Rails
+
+# Checks "data-remote" if true to handle the request through a XHR request.
+isRemote = (element) ->
+ value = element.getAttribute('data-remote')
+ value? and value isnt 'false'
+
+# Submits "remote" forms and links with ajax
+Rails.handleRemote = (e) ->
+ element = this
+
+ return true unless isRemote(element)
+ unless fire(element, 'ajax:before')
+ fire(element, 'ajax:stopped')
+ return false
+
+ withCredentials = element.getAttribute('data-with-credentials')
+ dataType = element.getAttribute('data-type') or 'script'
+
+ if matches(element, Rails.formSubmitSelector)
+ # memoized value from clicked submit button
+ button = getData(element, 'ujs:submit-button')
+ method = getData(element, 'ujs:submit-button-formmethod') or element.method
+ url = getData(element, 'ujs:submit-button-formaction') or element.getAttribute('action') or location.href
+
+ # strip query string if it's a GET request
+ url = url.replace(/\?.*$/, '') if method.toUpperCase() is 'GET'
+
+ if element.enctype is 'multipart/form-data'
+ data = new FormData(element)
+ data.append(button.name, button.value) if button?
+ else
+ data = serializeElement(element, button)
+
+ setData(element, 'ujs:submit-button', null)
+ setData(element, 'ujs:submit-button-formmethod', null)
+ setData(element, 'ujs:submit-button-formaction', null)
+ else if matches(element, Rails.buttonClickSelector) or matches(element, Rails.inputChangeSelector)
+ method = element.getAttribute('data-method')
+ url = element.getAttribute('data-url')
+ data = serializeElement(element, element.getAttribute('data-params'))
+ else
+ method = element.getAttribute('data-method')
+ url = Rails.href(element)
+ data = element.getAttribute('data-params')
+
+ ajax(
+ type: method or 'GET'
+ url: url
+ data: data
+ dataType: dataType
+ # stopping the "ajax:beforeSend" event will cancel the ajax request
+ beforeSend: (xhr, options) ->
+ if fire(element, 'ajax:beforeSend', [xhr, options])
+ fire(element, 'ajax:send', [xhr])
+ else
+ fire(element, 'ajax:stopped')
+ xhr.abort()
+ success: (args...) -> fire(element, 'ajax:success', args)
+ error: (args...) -> fire(element, 'ajax:error', args)
+ complete: (args...) -> fire(element, 'ajax:complete', args)
+ crossDomain: isCrossDomain(url)
+ withCredentials: withCredentials? and withCredentials isnt 'false'
+ )
+ stopEverything(e)
+
+Rails.formSubmitButtonClick = (e) ->
+ button = this
+ form = button.form
+ return unless form
+ # Register the pressed submit button
+ setData(form, 'ujs:submit-button', name: button.name, value: button.value) if button.name
+ # Save attributes from button
+ setData(form, 'ujs:formnovalidate-button', button.formNoValidate)
+ setData(form, 'ujs:submit-button-formaction', button.getAttribute('formaction'))
+ setData(form, 'ujs:submit-button-formmethod', button.getAttribute('formmethod'))
+
+Rails.handleMetaClick = (e) ->
+ link = this
+ method = (link.getAttribute('data-method') or 'GET').toUpperCase()
+ data = link.getAttribute('data-params')
+ metaClick = e.metaKey or e.ctrlKey
+ e.stopImmediatePropagation() if metaClick and method is 'GET' and not data
diff --git a/actionview/app/assets/javascripts/rails-ujs.coffee b/actionview/app/assets/javascripts/rails-ujs.coffee
new file mode 100644
index 0000000000..afe7d6f7a3
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs.coffee
@@ -0,0 +1,80 @@
+#
+# Unobtrusive JavaScript
+# https://github.com/rails/rails-ujs
+#
+# Released under the MIT license
+#
+#= require ./config
+#= require_tree ./utils
+#= require_tree ./features
+
+{
+ fire, delegate
+ getData, $
+ refreshCSRFTokens, CSRFProtection
+ enableElement, disableElement, handleDisabledElement
+ handleConfirm
+ handleRemote, formSubmitButtonClick, handleMetaClick
+ handleMethod
+} = Rails
+
+# For backward compatibility
+if jQuery? and not jQuery.rails
+ jQuery.rails = Rails
+ jQuery.ajaxPrefilter (options, originalOptions, xhr) ->
+ CSRFProtection(xhr) unless options.crossDomain
+
+Rails.start = ->
+ # Cut down on the number of issues from people inadvertently including
+ # rails-ujs twice by detecting and raising an error when it happens.
+ throw new Error('rails-ujs has already been loaded!') if window._rails_loaded
+
+ # This event works the same as the load event, except that it fires every
+ # time the page is loaded.
+ # See https://github.com/rails/jquery-ujs/issues/357
+ # See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching
+ window.addEventListener 'pageshow', ->
+ $(Rails.formEnableSelector).forEach (el) ->
+ enableElement(el) if getData(el, 'ujs:disabled')
+ $(Rails.linkDisableSelector).forEach (el) ->
+ enableElement(el) if getData(el, 'ujs:disabled')
+
+ delegate document, Rails.linkDisableSelector, 'ajax:complete', enableElement
+ delegate document, Rails.linkDisableSelector, 'ajax:stopped', enableElement
+ delegate document, Rails.buttonDisableSelector, 'ajax:complete', enableElement
+ delegate document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement
+
+ delegate document, Rails.linkClickSelector, 'click', handleDisabledElement
+ delegate document, Rails.linkClickSelector, 'click', handleConfirm
+ delegate document, Rails.linkClickSelector, 'click', handleMetaClick
+ delegate document, Rails.linkClickSelector, 'click', disableElement
+ delegate document, Rails.linkClickSelector, 'click', handleRemote
+ delegate document, Rails.linkClickSelector, 'click', handleMethod
+
+ delegate document, Rails.buttonClickSelector, 'click', handleDisabledElement
+ delegate document, Rails.buttonClickSelector, 'click', handleConfirm
+ delegate document, Rails.buttonClickSelector, 'click', disableElement
+ delegate document, Rails.buttonClickSelector, 'click', handleRemote
+
+ delegate document, Rails.inputChangeSelector, 'change', handleDisabledElement
+ delegate document, Rails.inputChangeSelector, 'change', handleConfirm
+ delegate document, Rails.inputChangeSelector, 'change', handleRemote
+
+ delegate document, Rails.formSubmitSelector, 'submit', handleDisabledElement
+ delegate document, Rails.formSubmitSelector, 'submit', handleConfirm
+ delegate document, Rails.formSubmitSelector, 'submit', handleRemote
+ # Normal mode submit
+ # Slight timeout so that the submit button gets properly serialized
+ delegate document, Rails.formSubmitSelector, 'submit', (e) -> setTimeout((-> disableElement(e)), 13)
+ delegate document, Rails.formSubmitSelector, 'ajax:send', disableElement
+ delegate document, Rails.formSubmitSelector, 'ajax:complete', enableElement
+
+ delegate document, Rails.formInputClickSelector, 'click', handleDisabledElement
+ delegate document, Rails.formInputClickSelector, 'click', handleConfirm
+ delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick
+
+ document.addEventListener('DOMContentLoaded', refreshCSRFTokens)
+ window._rails_loaded = true
+
+if window.Rails is Rails and fire(document, 'rails:attachBindings')
+ Rails.start()
diff --git a/actionview/app/assets/javascripts/utils/ajax.coffee b/actionview/app/assets/javascripts/utils/ajax.coffee
new file mode 100644
index 0000000000..9af515beda
--- /dev/null
+++ b/actionview/app/assets/javascripts/utils/ajax.coffee
@@ -0,0 +1,95 @@
+#= require ./csrf
+#= require ./event
+
+{ CSRFProtection, fire } = Rails
+
+AcceptHeaders =
+ '*': '*/*'
+ text: 'text/plain'
+ html: 'text/html'
+ xml: 'application/xml, text/xml'
+ json: 'application/json, text/javascript'
+ script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript'
+
+Rails.ajax = (options) ->
+ options = prepareOptions(options)
+ xhr = createXHR options, ->
+ response = processResponse(xhr.response, xhr.getResponseHeader('Content-Type'))
+ if xhr.status // 100 == 2
+ options.success?(response, xhr.statusText, xhr)
+ else
+ options.error?(response, xhr.statusText, xhr)
+ options.complete?(xhr, xhr.statusText)
+ # Call beforeSend hook
+ options.beforeSend?(xhr, options)
+ # Send the request
+ if xhr.readyState is XMLHttpRequest.OPENED
+ xhr.send(options.data)
+ else
+ fire(document, 'ajaxStop') # to be compatible with jQuery.ajax
+
+prepareOptions = (options) ->
+ options.type = options.type.toUpperCase()
+ # append data to url if it's a GET request
+ if options.type is 'GET' and options.data
+ if options.url.indexOf('?') < 0
+ options.url += '?' + options.data
+ else
+ options.url += '&' + options.data
+ # Use "*" as default dataType
+ options.dataType = '*' unless AcceptHeaders[options.dataType]?
+ options.accept = AcceptHeaders[options.dataType]
+ options.accept += ', */*; q=0.01' if options.dataType isnt '*'
+ options
+
+createXHR = (options, done) ->
+ xhr = new XMLHttpRequest()
+ # Open and setup xhr
+ xhr.open(options.type, options.url, true)
+ xhr.setRequestHeader('Accept', options.accept)
+ # Set Content-Type only when sending a string
+ # Sending FormData will automatically set Content-Type to multipart/form-data
+ if typeof options.data is 'string'
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8')
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') unless options.crossDomain
+ # Add X-CSRF-Token
+ CSRFProtection(xhr)
+ xhr.withCredentials = !!options.withCredentials
+ xhr.onreadystatechange = ->
+ done(xhr) if xhr.readyState is XMLHttpRequest.DONE
+ xhr
+
+processResponse = (response, type) ->
+ if typeof response is 'string' and typeof type is 'string'
+ if type.match(/\bjson\b/)
+ try response = JSON.parse(response)
+ else if type.match(/\bjavascript\b/)
+ script = document.createElement('script')
+ script.innerHTML = response
+ document.body.appendChild(script)
+ else if type.match(/\b(xml|html|svg)\b/)
+ parser = new DOMParser()
+ type = type.replace(/;.+/, '') # remove something like ';charset=utf-8'
+ try response = parser.parseFromString(response, type)
+ response
+
+# Default way to get an element's href. May be overridden at Rails.href.
+Rails.href = (element) -> element.href
+
+# Determines if the request is a cross domain request.
+Rails.isCrossDomain = (url) ->
+ originAnchor = document.createElement('a')
+ originAnchor.href = location.href
+ urlAnchor = document.createElement('a')
+ try
+ urlAnchor.href = url
+ # If URL protocol is false or is a string containing a single colon
+ # *and* host are false, assume it is not a cross-domain request
+ # (should only be the case for IE7 and IE compatibility mode).
+ # Otherwise, evaluate protocol and host of the URL against the origin
+ # protocol and host.
+ !(((!urlAnchor.protocol || urlAnchor.protocol == ':') && !urlAnchor.host) ||
+ (originAnchor.protocol + '//' + originAnchor.host == urlAnchor.protocol + '//' + urlAnchor.host))
+ catch e
+ # If there is an error parsing the URL, assume it is crossDomain.
+ true
diff --git a/actionview/app/assets/javascripts/utils/csrf.coffee b/actionview/app/assets/javascripts/utils/csrf.coffee
new file mode 100644
index 0000000000..4eb5ebb414
--- /dev/null
+++ b/actionview/app/assets/javascripts/utils/csrf.coffee
@@ -0,0 +1,25 @@
+#= require ./dom
+
+{ $ } = Rails
+
+# Up-to-date Cross-Site Request Forgery token
+csrfToken = Rails.csrfToken = ->
+ meta = document.querySelector('meta[name=csrf-token]')
+ meta and meta.content
+
+# URL param that must contain the CSRF token
+csrfParam = Rails.csrfParam = ->
+ meta = document.querySelector('meta[name=csrf-param]')
+ meta and meta.content
+
+# Make sure that every Ajax request sends the CSRF token
+Rails.CSRFProtection = (xhr) ->
+ token = csrfToken()
+ xhr.setRequestHeader('X-CSRF-Token', token) if token?
+
+# Make sure that all forms have actual up-to-date tokens (cached forms contain old ones)
+Rails.refreshCSRFTokens = ->
+ token = csrfToken()
+ param = csrfParam()
+ if token? and param?
+ $('form input[name="' + param + '"]').forEach (input) -> input.value = token
diff --git a/actionview/app/assets/javascripts/utils/dom.coffee b/actionview/app/assets/javascripts/utils/dom.coffee
new file mode 100644
index 0000000000..6bef618147
--- /dev/null
+++ b/actionview/app/assets/javascripts/utils/dom.coffee
@@ -0,0 +1,28 @@
+m = Element.prototype.matches or
+ Element.prototype.matchesSelector or
+ Element.prototype.mozMatchesSelector or
+ Element.prototype.msMatchesSelector or
+ Element.prototype.oMatchesSelector or
+ Element.prototype.webkitMatchesSelector
+
+Rails.matches = (element, selector) ->
+ if selector.exclude?
+ m.call(element, selector.selector) and not m.call(element, selector.exclude)
+ else
+ m.call(element, selector)
+
+# get and set data on a given element using "expando properties"
+# See: https://developer.mozilla.org/en-US/docs/Glossary/Expando
+expando = '_ujsData'
+
+Rails.getData = (element, key) ->
+ element[expando]?[key]
+
+Rails.setData = (element, key, value) ->
+ element[expando] ?= {}
+ element[expando][key] = value
+
+# a wrapper for document.querySelectorAll
+# returns an Array
+Rails.$ = (selector) ->
+ Array.prototype.slice.call(document.querySelectorAll(selector))
diff --git a/actionview/app/assets/javascripts/utils/event.coffee b/actionview/app/assets/javascripts/utils/event.coffee
new file mode 100644
index 0000000000..8d3ff007ea
--- /dev/null
+++ b/actionview/app/assets/javascripts/utils/event.coffee
@@ -0,0 +1,40 @@
+#= require ./dom
+
+{ matches } = Rails
+
+# Polyfill for CustomEvent in IE9+
+# https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
+CustomEvent = window.CustomEvent
+
+if typeof CustomEvent isnt 'function'
+ CustomEvent = (event, params) ->
+ evt = document.createEvent('CustomEvent')
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail)
+ evt
+ CustomEvent.prototype = window.Event.prototype
+
+# Triggers a custom event on an element and returns false if the event result is false
+fire = Rails.fire = (obj, name, data) ->
+ event = new CustomEvent(
+ name,
+ bubbles: true,
+ cancelable: true,
+ detail: data,
+ )
+ obj.dispatchEvent(event)
+ !event.defaultPrevented
+
+# Helper function, needed to provide consistent behavior in IE
+Rails.stopEverything = (e) ->
+ fire(e.target, 'ujs:everythingStopped')
+ e.preventDefault()
+ e.stopPropagation()
+ e.stopImmediatePropagation()
+
+Rails.delegate = (element, selector, eventType, handler) ->
+ element.addEventListener eventType, (e) ->
+ target = e.target
+ target = target.parentNode until not (target instanceof Element) or matches(target, selector)
+ if target instanceof Element and handler.call(target, e) == false
+ e.preventDefault()
+ e.stopPropagation()
diff --git a/actionview/app/assets/javascripts/utils/form.coffee b/actionview/app/assets/javascripts/utils/form.coffee
new file mode 100644
index 0000000000..5fa337b518
--- /dev/null
+++ b/actionview/app/assets/javascripts/utils/form.coffee
@@ -0,0 +1,36 @@
+#= require ./dom
+
+{ matches } = Rails
+
+toArray = (e) -> Array.prototype.slice.call(e)
+
+Rails.serializeElement = (element, additionalParam) ->
+ inputs = [element]
+ inputs = toArray(element.elements) if matches(element, 'form')
+ params = []
+
+ inputs.forEach (input) ->
+ return unless input.name
+ if matches(input, 'select')
+ toArray(input.options).forEach (option) ->
+ params.push(name: input.name, value: option.value) if option.selected
+ else if input.checked or ['radio', 'checkbox', 'submit'].indexOf(input.type) == -1
+ params.push(name: input.name, value: input.value)
+
+ params.push(additionalParam) if additionalParam
+
+ params.map (param) ->
+ if param.name?
+ "#{encodeURIComponent(param.name)}=#{encodeURIComponent(param.value)}"
+ else
+ param
+ .join('&')
+
+# Helper function that returns form elements that match the specified CSS selector
+# If form is actually a "form" element this will return associated elements outside the from that have
+# the html form attribute set
+Rails.formElements = (form, selector) ->
+ if matches(form, 'form')
+ toArray(form.elements).filter (el) -> matches(el, selector)
+ else
+ toArray(form.querySelectorAll(selector))
diff --git a/actionview/bin/test b/actionview/bin/test
index 84a05bba08..a7beb14b27 100755
--- a/actionview/bin/test
+++ b/actionview/bin/test
@@ -2,5 +2,3 @@
COMPONENT_ROOT = File.expand_path("..", __dir__)
require File.expand_path("../tools/test", COMPONENT_ROOT)
-
-exit Minitest.run(ARGV)
diff --git a/actionview/blade.yml b/actionview/blade.yml
new file mode 100644
index 0000000000..9e5eb953a4
--- /dev/null
+++ b/actionview/blade.yml
@@ -0,0 +1,11 @@
+load_paths:
+ - app/assets/javascripts
+
+logical_paths:
+ - rails-ujs.js
+
+build:
+ logical_paths:
+ - rails-ujs.js
+ path: lib/assets/compiled
+ clean: true
diff --git a/actionview/coffeelint.json b/actionview/coffeelint.json
new file mode 100644
index 0000000000..cf8bf2171b
--- /dev/null
+++ b/actionview/coffeelint.json
@@ -0,0 +1,135 @@
+{
+ "arrow_spacing": {
+ "level": "warn"
+ },
+ "braces_spacing": {
+ "level": "warn",
+ "spaces": 1,
+ "empty_object_spaces": 0
+ },
+ "camel_case_classes": {
+ "level": "error"
+ },
+ "coffeescript_error": {
+ "level": "error"
+ },
+ "colon_assignment_spacing": {
+ "level": "warn",
+ "spacing": {
+ "left": 0,
+ "right": 1
+ }
+ },
+ "cyclomatic_complexity": {
+ "level": "warn",
+ "value": 10
+ },
+ "duplicate_key": {
+ "level": "error"
+ },
+ "empty_constructor_needs_parens": {
+ "level": "warn"
+ },
+ "ensure_comprehensions": {
+ "level": "warn"
+ },
+ "eol_last": {
+ "level": "warn"
+ },
+ "indentation": {
+ "value": 2,
+ "level": "error"
+ },
+ "line_endings": {
+ "level": "warn",
+ "value": "unix"
+ },
+ "max_line_length": {
+ "value": 80,
+ "level": "ignore",
+ "limitComments": true
+ },
+ "missing_fat_arrows": {
+ "level": "ignore"
+ },
+ "newlines_after_classes": {
+ "value": 3,
+ "level": "warn"
+ },
+ "no_backticks": {
+ "level": "error"
+ },
+ "no_debugger": {
+ "level": "warn",
+ "console": false
+ },
+ "no_empty_functions": {
+ "level": "warn"
+ },
+ "no_empty_param_list": {
+ "level": "warn"
+ },
+ "no_implicit_braces": {
+ "level": "ignore",
+ "strict": true
+ },
+ "no_implicit_parens": {
+ "level": "ignore",
+ "strict": true
+ },
+ "no_interpolation_in_single_quotes": {
+ "level": "warn"
+ },
+ "no_nested_string_interpolation": {
+ "level": "warn"
+ },
+ "no_plusplus": {
+ "level": "warn"
+ },
+ "no_private_function_fat_arrows": {
+ "level": "warn"
+ },
+ "no_stand_alone_at": {
+ "level": "warn"
+ },
+ "no_tabs": {
+ "level": "error"
+ },
+ "no_this": {
+ "level": "warn"
+ },
+ "no_throwing_strings": {
+ "level": "error"
+ },
+ "no_trailing_semicolons": {
+ "level": "error"
+ },
+ "no_trailing_whitespace": {
+ "level": "error",
+ "allowed_in_comments": false,
+ "allowed_in_empty_lines": true
+ },
+ "no_unnecessary_double_quotes": {
+ "level": "warn"
+ },
+ "no_unnecessary_fat_arrows": {
+ "level": "warn"
+ },
+ "non_empty_constructor_needs_parens": {
+ "level": "warn"
+ },
+ "prefer_english_operator": {
+ "level": "ignore",
+ "doubleNotLevel": "warn"
+ },
+ "space_operators": {
+ "level": "warn"
+ },
+ "spacing_after_comma": {
+ "level": "warn"
+ },
+ "transform_messes_up_line_numbers": {
+ "level": "warn"
+ }
+}
+
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index ba6755be82..ca586f1d52 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2017 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/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index b7c05fdb88..5387174467 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -11,7 +11,7 @@ module ActionView #:nodoc:
# = Action View Base
#
# Action View templates can be written in several ways.
- # If the template file has a <tt>.erb</tt> extension, then it uses the erubis[https://rubygems.org/gems/erubis]
+ # If the template file has a <tt>.erb</tt> extension, then it uses the erubi[https://rubygems.org/gems/erubi]
# template system which can embed Ruby into an HTML document.
# If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used.
#
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 0658d8601d..ba189e23fe 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -62,8 +62,10 @@ module ActionView
node
end
else
- logger.error " '#{name}' file doesn't exist, so no dependencies"
- logger.error " Couldn't find template for digesting: #{name}"
+ unless name.include?('#') # Dynamic template partial names can never be tracked
+ logger.error " Couldn't find template for digesting: #{name}"
+ end
+
seen[name] ||= Missing.new(name, logical_name, nil)
end
end
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index 5fc4f3f1b9..92e21d7a4f 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -6,7 +6,7 @@ module ActionView
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index 4e4f4823e6..750f96f29e 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -35,18 +35,37 @@ 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.
#
+ # ==== 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
+ # that path.
+ # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
+ # when it is set to true.
+ #
+ # ==== Examples
+ #
# javascript_include_tag "xmlhr"
- # # => <script src="/assets/xmlhr.js?1284139606"></script>
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
+ #
+ # javascript_include_tag "xmlhr", host: "localhost", protocol: "https"
+ # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
#
# javascript_include_tag "template.jst", extname: false
- # # => <script src="/assets/template.jst?1284139606"></script>
+ # # => <script src="/assets/template.debug-1284139606.jst"></script>
#
# javascript_include_tag "xmlhr.js"
- # # => <script src="/assets/xmlhr.js?1284139606"></script>
+ # # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
#
# javascript_include_tag "common.javascript", "/elsewhere/cools"
- # # => <script src="/assets/common.javascript?1284139606"></script>
- # # <script src="/elsewhere/cools.js?1423139606"></script>
+ # # => <script src="/assets/common.javascript.debug-1284139606.js"></script>
+ # # <script src="/elsewhere/cools.debug-1284139606.js"></script>
#
# javascript_include_tag "http://www.example.com/xmlhr"
# # => <script src="http://www.example.com/xmlhr"></script>
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index c6a5e04aba..03bd1eb008 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -237,7 +237,11 @@ module ActionView
def compute_asset_extname(source, options = {})
return if options[:extname] == false
extname = options[:extname] || ASSET_EXTENSIONS[options[:type]]
- extname if extname && File.extname(source) != extname
+ if extname && File.extname(source) != extname
+ extname
+ else
+ nil
+ end
end
# Maps asset types to public directory.
@@ -406,7 +410,7 @@ module ActionView
def video_url(source, options = {})
url_to_asset(source, { type: :video }.merge!(options))
end
- alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route
+ alias_method :url_to_video, :video_url # aliased to avoid conflicts with a video_url named route
# Computes the path to an audio asset in the public audios directory.
# Full paths from the document root will be passed through.
@@ -445,7 +449,7 @@ module ActionView
def font_path(source, options = {})
path_to_asset(source, { type: :font }.merge!(options))
end
- alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route
+ alias_method :path_to_font, :font_path # aliased to avoid conflicts with a font_path named route
# Computes the full URL to a font asset.
# This will use +font_path+ internally, so most of their behaviors will be the same.
@@ -457,7 +461,7 @@ module ActionView
def font_url(source, options = {})
url_to_asset(source, { type: :font }.merge!(options))
end
- alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route
+ alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route
end
end
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index bf1c8ceaed..15ab7e304f 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -215,7 +215,7 @@ module ActionView
private
- def fragment_name_with_digest(name, virtual_path) #:nodoc:
+ def fragment_name_with_digest(name, virtual_path)
virtual_path ||= @virtual_path
if virtual_path
name = controller.url_for(name).split("://").last if name.is_a?(Hash)
@@ -226,7 +226,7 @@ module ActionView
end
end
- def fragment_for(name = {}, options = nil, &block) #:nodoc:
+ def fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
@cache_hit = true
content
@@ -236,13 +236,11 @@ module ActionView
end
end
- def read_fragment_for(name, options) #:nodoc:
+ def read_fragment_for(name, options)
controller.read_fragment(name, options)
end
- def write_fragment_for(name, options) #:nodoc:
- # VIEW TODO: Make #capture usable outside of ERB
- # This dance is needed because Builder can't use capture
+ def write_fragment_for(name, options)
pos = output_buffer.length
yield
output_safe = output_buffer.html_safe?
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 9bffe860db..26a625e4fe 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -474,6 +474,266 @@ module ActionView
end
private :apply_form_for_options!
+ # Creates a form tag based on mixing URLs, scopes, or models.
+ #
+ # # Using just a URL:
+ # <%= form_with url: posts_path do |form| %>
+ # <%= form.text_field :title %>
+ # <% end %>
+ # # =>
+ # <form action="/posts" method="post" data-remote="true">
+ # <input type="text" name="title">
+ # </form>
+ #
+ # # Adding a scope prefixes the input field names:
+ # <%= form_with scope: :post, url: posts_path do |form| %>
+ # <%= form.text_field :title %>
+ # <% end %>
+ # # =>
+ # <form action="/posts" method="post" data-remote="true">
+ # <input type="text" name="post[title]">
+ # </form>
+ #
+ # # Using a model infers both the URL and scope:
+ # <%= form_with model: Post.new do |form| %>
+ # <%= form.text_field :title %>
+ # <% end %>
+ # # =>
+ # <form action="/posts" method="post" data-remote="true">
+ # <input type="text" name="post[title]">
+ # </form>
+ #
+ # # An existing model makes an update form and fills out field values:
+ # <%= form_with model: Post.first do |form| %>
+ # <%= form.text_field :title %>
+ # <% end %>
+ # # =>
+ # <form action="/posts/1" method="post" data-remote="true">
+ # <input type="hidden" name="_method" value="patch">
+ # <input type="text" name="post[title]" value="<the title of the post>">
+ # </form>
+ #
+ # # Though the fields don't have to correspond to model attributes:
+ # <%= form_with model: Cat.new do |form| %>
+ # <%= form.text_field :cats_dont_have_gills %>
+ # <%= form.text_field :but_in_forms_they_can %>
+ # <% end %>
+ # # =>
+ # <form action="/cats" method="post" data-remote="true">
+ # <input type="text" name="cat[cats_dont_have_gills]">
+ # <input type="text" name="cat[but_in_forms_they_can]">
+ # </form>
+ #
+ # The parameters in the forms are accessible in controllers according to
+ # their name nesting. So inputs named +title+ and <tt>post[title]</tt> are
+ # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt>
+ # respectively.
+ #
+ # By default +form_with+ attaches the <tt>data-remote</tt> attribute
+ # submitting the form via an XMLHTTPRequest in the background if an
+ # Unobtrusive JavaScript driver, like rails-ujs, is used. See the
+ # <tt>:local</tt> option for more.
+ #
+ # For ease of comparison the examples above left out the submit button,
+ # as well as the auto generated hidden fields that enable UTF-8 support
+ # and adds an authenticity token needed for cross site request forgery
+ # protection.
+ #
+ # ==== +form_with+ options
+ #
+ # * <tt>:url</tt> - The URL the form submits to. Akin to values passed to
+ # +url_for+ or +link_to+. For example, you may use a named route
+ # directly. When a <tt>:scope</tt> is passed without a <tt>:url</tt> the
+ # form just submits to the current URL.
+ # * <tt>:method</tt> - The method to use when submitting the form, usually
+ # either "get" or "post". If "patch", "put", "delete", or another verb
+ # is used, a hidden input named <tt>_method</tt> is added to
+ # simulate the verb over post.
+ # * <tt>:format</tt> - The format of the route the form submits to.
+ # Useful when submitting to another resource type, like <tt>:json</tt>.
+ # Skipped if a <tt>:url</tt> is passed.
+ # * <tt>:scope</tt> - The scope to prefix input field names with and
+ # thereby how the submitted parameters are grouped in controllers.
+ # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and
+ # <tt>:scope</tt> by, plus fill out input field values.
+ # So if a +title+ attribute is set to "Ahoy!" then a +title+ input
+ # field's value would be "Ahoy!".
+ # If the model is a new record a create form is generated, if an
+ # existing record, however, an update form is generated.
+ # Pass <tt>:scope</tt> or <tt>:url</tt> to override the defaults.
+ # E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>.
+ # * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
+ # Override with a custom authenticity token or pass <tt>false</tt> to
+ # skip the authenticity token field altogether.
+ # Useful when submitting to an external resource like a payment gateway
+ # that might limit the valid fields.
+ # Remote forms may omit the embedded authenticity token by setting
+ # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
+ # 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.
+ # 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>:builder</tt> - Override the object used to build the form.
+ # * <tt>:id</tt> - Optional HTML id attribute.
+ # * <tt>:class</tt> - Optional HTML class attribute.
+ # * <tt>:data</tt> - Optional HTML data attributes.
+ # * <tt>:html</tt> - Other optional HTML attributes for the form tag.
+ #
+ # === Examples
+ #
+ # When not passing a block, +form_with+ just generates an opening form tag.
+ #
+ # <%= form_with(model: @post, url: super_posts_path) %>
+ # <%= form_with(model: @post, scope: :article) %>
+ # <%= form_with(model: @post, format: :json) %>
+ # <%= form_with(model: @post, authenticity_token: false) %> # Disables the token.
+ #
+ # For namespaced routes, like +admin_post_url+:
+ #
+ # <%= form_with(model: [ :admin, @post ]) do |form| %>
+ # ...
+ # <% end %>
+ #
+ # If your resource has associations defined, for example, you want to add comments
+ # to the document given that the routes are set correctly:
+ #
+ # <%= form_with(model: [ @document, Comment.new ]) do |form| %>
+ # ...
+ # <% end %>
+ #
+ # 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
+ # match the stand-alone FormHelper methods and methods
+ # from FormTagHelper:
+ #
+ # <%= form_with scope: :person do |form| %>
+ # <%= form.text_field :first_name %>
+ # <%= form.text_field :last_name %>
+ #
+ # <%= text_area :person, :biography %>
+ # <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
+ #
+ # <%= form.submit %>
+ # <% end %>
+ #
+ # Same goes for the methods in FormOptionHelper and DateHelper designed
+ # to work with an object as a base, like
+ # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ #
+ # === Setting the method
+ #
+ # You can force the form to use the full array of HTTP verbs by setting
+ #
+ # method: (:get|:post|:patch|:put|:delete)
+ #
+ # in the options hash. If the verb is not GET or POST, which are natively
+ # supported by HTML forms, the form will be set to POST and a hidden input
+ # called _method will carry the intended verb for the server to interpret.
+ #
+ # === Setting HTML options
+ #
+ # You can set data attributes directly in a data hash, but HTML options
+ # besides id and class must be wrapped in an HTML key:
+ #
+ # <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %>
+ # ...
+ # <% end %>
+ #
+ # generates
+ #
+ # <form action="/posts/123" method="post" data-behavior="autosave" name="go">
+ # <input name="_method" type="hidden" value="patch" />
+ # ...
+ # </form>
+ #
+ # === Removing hidden model id's
+ #
+ # The +form_with+ method automatically includes the model id as a hidden field in the form.
+ # This is used to maintain the correlation between the form data and its associated model.
+ # Some ORM systems do not use IDs on nested models so in this case you want to be able
+ # to disable the hidden id.
+ #
+ # In the following example the Post model has many Comments stored within it in a NoSQL database,
+ # thus there is no primary key for comments.
+ #
+ # <%= form_with(model: @post) do |form| %>
+ # <%= form.fields(:comments, skip_id: true) do |fields| %>
+ # ...
+ # <% end %>
+ # <% end %>
+ #
+ # === Customized form builders
+ #
+ # You can also build forms using a customized FormBuilder class. Subclass
+ # FormBuilder and override or define some more helpers, then use your
+ # custom builder. For example, let's say you made a helper to
+ # automatically add labels to form inputs.
+ #
+ # <%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %>
+ # <%= form.text_field :first_name %>
+ # <%= form.text_field :last_name %>
+ # <%= form.text_area :biography %>
+ # <%= form.check_box :admin %>
+ # <%= form.submit %>
+ # <% end %>
+ #
+ # In this case, if you use:
+ #
+ # <%= render form %>
+ #
+ # The rendered template is <tt>people/_labelling_form</tt> and the local
+ # variable referencing the form builder is called
+ # <tt>labelling_form</tt>.
+ #
+ # The custom FormBuilder class is automatically merged with the options
+ # of a nested +fields+ call, unless it's explicitly set.
+ #
+ # In many cases you will want to wrap the above in another helper, so you
+ # could do something like the following:
+ #
+ # def labelled_form_with(**options, &block)
+ # form_with(**options.merge(builder: LabellingFormBuilder), &block)
+ # 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
+
+ if model
+ url ||= polymorphic_path(model, format: format)
+
+ model = model.last if model.is_a?(Array)
+ scope ||= model_name_from_record_or_class(model).param_key
+ end
+
+ if block_given?
+ builder = instantiate_builder(scope, model, options)
+ output = capture(builder, &Proc.new)
+ options[:multipart] ||= builder.multipart?
+
+ html_options = html_options_for_form_with(url, model, options)
+ form_tag_with_body(html_options, output)
+ else
+ html_options = html_options_for_form_with(url, model, options)
+ form_tag_html(html_options)
+ end
+ end
+
# Creates a scope around a specific model object like form_for, but
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
@@ -720,6 +980,74 @@ module ActionView
capture(builder, &block)
end
+ # Scopes input fields with either an explicit scope or model.
+ # Like +form_with+ does with <tt>:scope</tt> or <tt>:model</tt>,
+ # except it doesn't output the form tags.
+ #
+ # # Using a scope prefixes the input field names:
+ # <%= fields :comment do |fields| %>
+ # <%= fields.text_field :body %>
+ # <% end %>
+ # # => <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.text_field :body %>
+ # <% end %>
+ # # =>
+ # <input type="text" name="comment[body] value="full bodied">
+ #
+ # # Using +fields+ with +form_with+:
+ # <%= form_with model: @post do |form| %>
+ # <%= form.text_field :title %>
+ #
+ # <%= form.fields :comment do |fields| %>
+ # <%= fields.text_field :body %>
+ # <% end %>
+ # <% end %>
+ #
+ # Much like +form_with+ a FormBuilder instance associated with the scope
+ # 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
+ # match the stand-alone FormHelper methods and methods
+ # from FormTagHelper:
+ #
+ # <%= fields model: @comment do |fields| %>
+ # <%= fields.text_field :body %>
+ #
+ # <%= text_area :commenter, :biography %>
+ # <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %>
+ # <% end %>
+ #
+ # Same goes for the methods in FormOptionHelper and DateHelper designed
+ # to work with an object as a base, like
+ # FormOptionHelper#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
+
+ if model
+ scope ||= model_name_from_record_or_class(model).param_key
+ end
+
+ builder = instantiate_builder(scope, model, options)
+ capture(builder, &block)
+ end
+
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
# is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
@@ -1175,6 +1503,32 @@ module ActionView
end
private
+ def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: false,
+ skip_enforcing_utf8: false, **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[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
+
+ # The following URL is unescaped, this is just a hash of options, and it is the
+ # responsibility of the caller to escape all the values.
+ html_options[:action] = url_for(url_for_options || {})
+ html_options[:"accept-charset"] = "UTF-8"
+ html_options[:"data-remote"] = true unless local
+
+ if !local && !embed_authenticity_token_in_remote_forms &&
+ html_options[:authenticity_token].blank?
+ # The authenticity token is taken from the meta tag in this case
+ html_options[:authenticity_token] = false
+ elsif html_options[:authenticity_token] == true
+ # Include the default authenticity_token, which is only generated when its set to nil,
+ # but we needed the true value to override the default of no authenticity_token on data-remote.
+ html_options[:authenticity_token] = nil
+ end
+
+ html_options.stringify_keys!
+ end
def instantiate_builder(record_name, record_object, options)
case record_name
@@ -1183,7 +1537,7 @@ module ActionView
object_name = record_name
else
object = record_name
- object_name = model_name_from_record_or_class(object).param_key
+ object_name = model_name_from_record_or_class(object).param_key if object
end
builder = options[:builder] || default_form_builder_class
@@ -1249,7 +1603,7 @@ module ActionView
# The methods which wrap a form helper call.
class_attribute :field_helpers
- self.field_helpers = [:fields_for, :label, :text_field, :password_field,
+ self.field_helpers = [:fields_for, :fields, :label, :text_field, :password_field,
:hidden_field, :file_field, :text_area, :check_box,
:radio_button, :color_field, :search_field,
:telephone_field, :phone_field, :date_field,
@@ -1285,7 +1639,10 @@ module ActionView
def initialize(object_name, object, template, options)
@nested_child_index = {}
@object_name, @object, @template, @options = object_name, object, template, options
- @default_options = @options ? @options.slice(:index, :namespace) : {}
+ @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {}
+
+ convert_to_legacy_options(@options)
+
if @object_name.to_s.match(/\[\]$/)
if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
@auto_index = object.to_param
@@ -1293,11 +1650,12 @@ module ActionView
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
+
@multipart = nil
@index = options[:index] || options[:child_index]
end
- (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector|
+ (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) # def text_field(method, options = {})
@template.send( # @template.send(
@@ -1566,10 +1924,11 @@ module ActionView
record_name = model_name_from_record_or_class(record_object).param_key
end
+ object_name = @object_name
index = if options.has_key?(:index)
options[:index]
elsif defined?(@auto_index)
- self.object_name = @object_name.to_s.sub(/\[\]$/, "")
+ object_name = object_name.to_s.sub(/\[\]$/, "")
@auto_index
end
@@ -1586,6 +1945,16 @@ module ActionView
@template.fields_for(record_name, record_object, fields_options, &block)
end
+ # 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
+
+ convert_to_legacy_options(options)
+
+ 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
# assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
# is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
@@ -1934,6 +2303,12 @@ module ActionView
@nested_child_index[name] ||= -1
@nested_child_index[name] += 1
end
+
+ def convert_to_legacy_options(options)
+ if options.key?(:skip_id)
+ options[:include_id] = !options.delete(:skip_id)
+ end
+ end
end
end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 7bd473507b..ffc64e7118 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -442,26 +442,14 @@ module ActionView
# # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
#
def submit_tag(value = "Save changes", options = {})
- options = options.stringify_keys
+ options = options.deep_stringify_keys
tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
-
- if ActionView::Base.automatically_disable_submit_tag
- unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false)
- disable_with_text = tag_options["data-disable-with"]
- disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"]
- disable_with_text ||= value.to_s.clone
- tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
- else
- tag_options["data"].delete(:disable_with) if tag_options["data"]
- end
- tag_options.delete("data-disable-with")
- end
-
+ set_default_disable_with value, tag_options
tag :input, tag_options
end
# Creates a button element that defines a <tt>submit</tt> button,
- # <tt>reset</tt>button or a generic button which can be used in
+ # <tt>reset</tt> button or a generic button which can be used in
# JavaScript, for example. You can use the button tag as a regular
# submit tag but it isn't supported in legacy browsers. However,
# the button tag does allow for richer labels such as images and emphasis,
@@ -898,6 +886,22 @@ module ActionView
def sanitize_to_id(name)
name.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_")
end
+
+ def set_default_disable_with(value, tag_options)
+ return unless ActionView::Base.automatically_disable_submit_tag
+ data = tag_options["data"]
+
+ unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false)
+ disable_with_text = tag_options["data-disable-with"]
+ disable_with_text ||= data["disable_with"] if data
+ disable_with_text ||= value.to_s.clone
+ tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
+ else
+ data.delete("disable_with") if data
+ end
+
+ tag_options.delete("data-disable-with")
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index 75b898c3e9..b6bc5f4f6f 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -92,7 +92,7 @@ module ActionView
# (defaults to "%u%n"). Fields are <tt>%u</tt> for the
# currency, and <tt>%n</tt> for the number.
# * <tt>:negative_format</tt> - Sets the format for negative
- # numbers (defaults to prepending an hyphen to the formatted
+ # numbers (defaults to prepending a hyphen to the formatted
# number given by <tt>:format</tt>). Accepts the same fields
# than <tt>:format</tt>, except <tt>%n</tt> is here the
# absolute value of the number.
@@ -171,6 +171,9 @@ module ActionView
# to ",").
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
+ # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for
+ # deriving the placement of delimiter. Helpful when using currency formats
+ # like INR.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
@@ -187,6 +190,9 @@ module ActionView
# number_with_delimiter(98765432.98, delimiter: " ", separator: ",")
# # => 98 765 432,98
#
+ # number_with_delimiter("123456.78",
+ # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) # => "1,23,456.78"
+ #
# number_with_delimiter("112a", raise: true) # => raise InvalidNumberError
def number_with_delimiter(number, options = {})
delegate_number_helper_method(:number_to_delimited, number, options)
diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb
index 9f1a890f6a..25defd1276 100644
--- a/actionview/lib/action_view/helpers/output_safety_helper.rb
+++ b/actionview/lib/action_view/helpers/output_safety_helper.rb
@@ -25,7 +25,7 @@ module ActionView #:nodoc:
# safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />")
# # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
#
- # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />")
+ # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />"))
# # => "<p>foo</p><br /><p>bar</p>"
#
def safe_join(array, sep = $,)
@@ -60,7 +60,7 @@ module ActionView #:nodoc:
when 2
safe_join([array[0], array[1]], options[:two_words_connector])
else
- safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]])
+ safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil)
end
end
end
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index 3d6ff598ee..0abd5bc5dc 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -13,6 +13,7 @@ module ActionView
# It also strips href/src attributes with unsafe protocols like
# <tt>javascript:</tt>, while also protecting against attempts to use Unicode,
# ASCII, and hex character references to work around these protocol filters.
+ # All special characters will be escaped.
#
# The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML
# Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
@@ -20,8 +21,7 @@ module ActionView
# Custom sanitization rules can also be provided.
#
# Please note that sanitizing user-provided text does not guarantee that the
- # resulting markup is valid or even well-formed. For example, the output may still
- # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>.
+ # resulting markup is valid or even well-formed.
#
# ==== Options
#
@@ -45,17 +45,15 @@ module ActionView
# Providing a custom Rails::Html scrubber:
#
# class CommentScrubber < Rails::Html::PermitScrubber
- # def allowed_node?(node)
- # !%w(form script comment blockquote).include?(node.name)
+ # def initialize
+ # super
+ # self.tags = %w( form script comment blockquote )
+ # self.attributes = %w( style )
# end
#
# def skip_node?(node)
# node.text?
# end
- #
- # def scrub_attribute?(name)
- # name == 'style'
- # end
# end
#
# <%= sanitize @comment.body, scrubber: CommentScrubber.new %>
@@ -88,7 +86,7 @@ module ActionView
self.class.white_list_sanitizer.sanitize_css(style)
end
- # Strips all HTML tags from +html+, including comments.
+ # Strips all HTML tags from +html+, including comments and special characters.
#
# strip_tags("Strip <i>these</i> tags!")
# # => Strip these tags!
@@ -98,8 +96,11 @@ module ActionView
#
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => Welcome to my website!
+ #
+ # strip_tags("> A quote from Smith & Wesson")
+ # # => &gt; A quote from Smith &amp; Wesson
def strip_tags(html)
- self.class.full_sanitizer.sanitize(html, encode_special_chars: false)
+ self.class.full_sanitizer.sanitize(html)
end
# Strips all link tags from +html+ leaving just the link text.
@@ -112,6 +113,9 @@ module ActionView
#
# strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
# # => Blog: Visit.
+ #
+ # strip_links('<<a href="https://example.org">malformed & link</a>')
+ # # => &lt;malformed &amp; link
def strip_links(html)
self.class.link_sanitizer.sanitize(html)
end
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index cf8a6d6028..0895533a60 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -13,8 +13,17 @@ module ActionView
@object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
@object = retrieve_object(options.delete(:object))
+ @skip_default_ids = options.delete(:skip_default_ids)
+ @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
@options = options
- @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil
+
+ if Regexp.last_match
+ @generate_indexed_names = true
+ @auto_index = retrieve_autoindex(Regexp.last_match.pre_match)
+ else
+ @generate_indexed_names = false
+ @auto_index = nil
+ end
end
# This is what child classes implement.
@@ -25,7 +34,11 @@ module ActionView
private
def value(object)
- object.public_send @method_name if object
+ if @allow_method_names_outside_object
+ object.public_send @method_name if object && object.respond_to?(@method_name)
+ else
+ object.public_send @method_name if object
+ end
end
def value_before_type_cast(object)
@@ -81,15 +94,21 @@ module ActionView
def add_default_name_and_id(options)
index = name_and_id_index(options)
options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
- options["id"] = options.fetch("id") { tag_id(index) }
- if namespace = options.delete("namespace")
- options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
+
+ unless skip_default_ids?
+ options["id"] = options.fetch("id") { tag_id(index) }
+ if namespace = options.delete("namespace")
+ options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
+ end
end
end
def tag_name(multiple = false, index = nil)
# a little duplication to construct less strings
- if index
+ case
+ when @object_name.empty?
+ "#{sanitized_method_name}#{"[]" if multiple}"
+ when index
"#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
else
"#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
@@ -98,7 +117,10 @@ module ActionView
def tag_id(index = nil)
# a little duplication to construct less strings
- if index
+ case
+ when @object_name.empty?
+ sanitized_method_name.dup
+ when index
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
else
"#{sanitized_object_name}_#{sanitized_method_name}"
@@ -152,7 +174,15 @@ module ActionView
end
def name_and_id_index(options)
- options.key?("index") ? options.delete("index") || "" : @auto_index
+ if options.key?("index")
+ options.delete("index") || ""
+ elsif @generate_indexed_names
+ @auto_index || ""
+ end
+ end
+
+ def skip_default_ids?
+ @skip_default_ids
end
end
end
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 0359d4e65d..7252d4f2d9 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -24,7 +24,7 @@ module ActionView
builder.check_box + builder.label
end
- def hidden_field_name #:nodoc:
+ def hidden_field_name
"#{super}[]"
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index c8be392865..75d237eb35 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -43,7 +43,7 @@ module ActionView
# Generate default options for collection helpers, such as :checked and
# :disabled.
- def default_html_options_for_collection(item, value) #:nodoc:
+ def default_html_options_for_collection(item, value)
html_options = @html_options.dup
[:checked, :selected, :disabled, :readonly].each do |option|
@@ -67,11 +67,11 @@ module ActionView
html_options
end
- def sanitize_attribute_name(value) #:nodoc:
+ def sanitize_attribute_name(value)
"#{sanitized_method_name}_#{sanitized_value(value)}"
end
- def render_collection #:nodoc:
+ def render_collection
@collection.map do |item|
value = value_for_collection(item, @value_method)
text = value_for_collection(item, @text_method)
@@ -82,7 +82,7 @@ module ActionView
end.join.html_safe
end
- def render_collection_for(builder_class, &block) #:nodoc:
+ def render_collection_for(builder_class, &block)
options = @options.stringify_keys
rendered_collection = render_collection do |item, value, text, default_html_options|
builder = instantiate_builder(builder_class, item, value, text, default_html_options)
@@ -103,12 +103,12 @@ module ActionView
end
end
- def hidden_field #:nodoc:
+ def hidden_field
hidden_name = @html_options[:name] || hidden_field_name
@template_object.hidden_field_tag(hidden_name, "", id: nil)
end
- def hidden_field_name #:nodoc:
+ def hidden_field_name
"#{tag_name(false, @options[:index])}"
end
end
diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb
index 006605885a..638c134deb 100644
--- a/actionview/lib/action_view/helpers/tags/date_select.rb
+++ b/actionview/lib/action_view/helpers/tags/date_select.rb
@@ -16,7 +16,7 @@ module ActionView
class << self
def select_type
- @select_type ||= self.name.split("::").last.sub("Select", "").downcase
+ @select_type ||= name.split("::").last.sub("Select", "").downcase
end
end
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index b31d5fda66..cab15ae201 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -73,6 +73,10 @@ 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/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
index 4306c3543d..613cade7b3 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -17,7 +17,7 @@ module ActionView
class << self
def field_type
- @field_type ||= self.name.split("::").last.sub("Field", "").downcase
+ @field_type ||= name.split("::").last.sub("Field", "").downcase
end
end
diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb
index 62b1df81c6..ced835eaa8 100644
--- a/actionview/lib/action_view/helpers/tags/translator.rb
+++ b/actionview/lib/action_view/helpers/tags/translator.rb
@@ -14,6 +14,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
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 07dccf5b41..bc922f9ce8 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -187,7 +187,7 @@ module ActionView
unless separator.empty?
text.split(separator).each do |value|
if value.match(regex)
- regex = phrase = value
+ phrase = value
break
end
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 1277126995..304db38060 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -35,7 +35,7 @@ module ActionView
when :back
_back_url
else
- raise ArgumentError, "arguments passed to url_for can't be handled. Please require " +
+ raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \
"routes or provide your own implementation"
end
end
@@ -105,10 +105,9 @@ module ActionView
# driver to prompt with the question specified (in this case, the
# resulting text would be <tt>question?</tt>. If the user accepts, the
# link is processed normally, otherwise no action is taken.
- # * <tt>:disable_with</tt> - Value of this parameter will be
- # used as the value for a disabled version of the submit
- # button when the form is submitted. This feature is provided
- # by the unobtrusive JavaScript driver.
+ # * <tt>:disable_with</tt> - Value of this parameter will be used as the
+ # name for a disabled version of the link. This feature is provided by
+ # the unobtrusive JavaScript driver.
#
# ==== Examples
# Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
@@ -518,6 +517,9 @@ module ActionView
# current_page?('http://www.example.com/shop/checkout')
# # => true
#
+ # current_page?('http://www.example.com/shop/checkout', check_parameters: true)
+ # # => false
+ #
# current_page?('/shop/checkout')
# # => true
#
@@ -531,7 +533,7 @@ module ActionView
#
# We can also pass in the symbol arguments instead of strings.
#
- def current_page?(options)
+ def current_page?(options, check_parameters: false)
unless request
raise "You cannot use helpers that need to determine the current " \
"page unless your view context provides a Request object " \
@@ -540,12 +542,14 @@ module ActionView
return false unless request.get? || request.head?
+ check_parameters ||= !options.is_a?(String) && options.try(:delete, :check_parameters)
url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
# We ignore any extra parameters in the request_uri if the
# submitted url doesn't have any either. This lets the function
# work with things like ?order=asc
- request_uri = url_string.index("?") ? request.fullpath : request.path
+ # the behaviour can be disabled with check_parameters: true
+ request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
url_string.chomp!("/") if url_string.start_with?("/") && url_string != "/"
@@ -615,10 +619,10 @@ module ActionView
#
# to_form_params({ name: 'Denmark' }, 'country')
# # => [{name: 'country[name]', value: 'Denmark'}]
- def to_form_params(attribute, namespace = nil) # :nodoc:
+ def to_form_params(attribute, namespace = nil)
attribute = if attribute.respond_to?(:permitted?)
unless attribute.permitted?
- raise ArgumentError, "Attempting to generate a buttom from non-sanitized request parameters!" \
+ raise ArgumentError, "Attempting to generate a button from non-sanitized request parameters!" \
" Whitelist and sanitize passed parameters to be secure."
end
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index 7499e3b951..81feb90486 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -319,7 +319,7 @@ module ActionView
name_clause
end
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _layout(formats)
if _conditional_layout?
#{layout_definition}
@@ -338,7 +338,7 @@ module ActionView
#
# ==== Returns
# * <tt>String</tt> - A template name
- def _implied_layout_name # :nodoc:
+ def _implied_layout_name
controller_path
end
end
diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb
index c9f308c2a2..d03e1a51b8 100644
--- a/actionview/lib/action_view/log_subscriber.rb
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -51,20 +51,20 @@ module ActionView
ActionView::Base.logger
end
- protected
+ private
EMPTY = ""
- def from_rails_root(string)
+ def from_rails_root(string) # :doc:
string = string.sub(rails_root, EMPTY)
string.sub!(VIEWS_PATTERN, EMPTY)
string
end
- def rails_root
+ def rails_root # :doc:
@root ||= "#{Rails.root}/"
end
- def render_count(payload)
+ def render_count(payload) # :doc:
if payload[:cache_hits]
"[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
else
@@ -72,7 +72,7 @@ module ActionView
end
end
- def cache_message(payload)
+ def cache_message(payload) # :doc:
if payload[:cache_hit]
"[cache hit]"
else
@@ -80,8 +80,6 @@ module ActionView
end
end
- private
-
def log_rendering_start(payload)
info do
message = " Rendering #{from_rails_root(payload[:identifier])}"
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 50faf1b8dd..f385a7cd04 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -93,9 +93,9 @@ module ActionView
@cache = old_value
end
- protected
+ private
- def _set_detail(key, value)
+ def _set_detail(key, value) # :doc:
@details = @details.dup if @details_key
@details_key = nil
@details[key] = value
@@ -149,16 +149,16 @@ module ActionView
added_resolvers.times { view_paths.pop }
end
- protected
+ private
- def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc:
+ def args_for_lookup(name, prefixes, partial, keys, details_options)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for(details_options)
[name, prefixes, partial || false, details, details_key, keys]
end
# Compute details hash and key according to user options (e.g. passed from #render).
- def detail_args_for(options)
+ def detail_args_for(options) # :doc:
return @details, details_key if options.empty? # most common path.
user_details = @details.merge(options)
@@ -171,13 +171,13 @@ module ActionView
[user_details, details_key]
end
- def args_for_any(name, prefixes, partial) # :nodoc:
+ def args_for_any(name, prefixes, partial)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for_any
[name, prefixes, partial || false, details, details_key]
end
- def detail_args_for_any # :nodoc:
+ def detail_args_for_any
@detail_args_for_any ||= begin
details = {}
@@ -200,7 +200,7 @@ module ActionView
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
- def normalize_name(name, prefixes) #:nodoc:
+ def normalize_name(name, prefixes)
prefixes = prefixes.presence
parts = name.to_s.split("/".freeze)
parts.shift if parts.first.empty?
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 68205cb720..d344d98f4b 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -3,7 +3,7 @@ require "rails"
module ActionView
# = Action View Railtie
- class Railtie < Rails::Railtie # :nodoc:
+ class Railtie < Rails::Engine # :nodoc:
config.action_view = ActiveSupport::OrderedOptions.new
config.action_view.embed_authenticity_token_in_remote_forms = false
config.action_view.debug_missing_translation = true
@@ -39,7 +39,7 @@ module ActionView
initializer "action_view.per_request_digest_cache" do |app|
ActiveSupport.on_load(:action_view) do
- if app.config.consider_all_requests_local
+ unless ActionView::Resolver.caching?
app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
end
end
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
index b39acfa0b5..48bea315a9 100644
--- a/actionview/lib/action_view/record_identifier.rb
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -92,7 +92,7 @@ module ActionView
end
end
- protected
+ private
# Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
# This can be overwritten to customize the default generated string representation if desired.
@@ -102,7 +102,7 @@ module ActionView
# overwritten version of the method. By default, this implementation passes the key string through a
# method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
# make sure yourself that your dom ids are valid, in case you overwrite this method.
- def record_key_for_dom_id(record)
+ def record_key_for_dom_id(record) # :doc:
key = convert_to_model(record).to_key
key ? key.join(JOIN) : key
end
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index 3c85be49cd..0b315eb569 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -25,9 +25,9 @@ module ActionView
raise NotImplementedError
end
- protected
+ private
- def extract_details(options)
+ def extract_details(options) # :doc:
@lookup_context.registered_details.each_with_object({}) do |key, details|
value = options[key]
@@ -35,7 +35,7 @@ module ActionView
end
end
- def instrument(name, **options)
+ def instrument(name, **options) # :doc:
options[:identifier] ||= (@template && @template.identifier) || @path
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
@@ -43,7 +43,7 @@ module ActionView
end
end
- def prepend_formats(formats)
+ def prepend_formats(formats) # :doc:
formats = Array(formats)
return if formats.empty? || @lookup_context.html_fallback_for_js
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index b8a79da97f..647b15ea94 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -99,7 +99,7 @@ module ActionView
# <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
#
# If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return nil. This will allow you
- # to specify a text which will displayed instead by using this form:
+ # to specify a text which will be displayed instead by using this form:
#
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
#
@@ -458,7 +458,7 @@ module ActionView
locals[counter] = index
locals[iteration] = partial_iteration
- template = (cache[path] ||= find_template(path, keys + [as, counter]))
+ template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
content = template.render(view, locals)
partial_iteration.iterate!
content
@@ -532,11 +532,11 @@ module ActionView
[variable, variable_counter, variable_iteration]
end
- IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
+ IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
"make sure your partial name starts with underscore."
- OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " +
- "make sure it starts with lowercase letter, " +
+ OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
+ "make sure it starts with lowercase letter, " \
"and is followed by any combination of letters, numbers and underscores."
def raise_invalid_identifier(path)
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 2434250b2d..62ce985243 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -4,7 +4,6 @@ module ActionView
# == TODO
#
# * Support streaming from child templates, partials and so on.
- # * Integrate exceptions with exceptron
# * Rack::Cache needs to support streaming bodies
class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
# A valid Rack::Body (i.e. it responds to each).
@@ -28,8 +27,7 @@ module ActionView
private
# This is the same logging logic as in ShowExceptions middleware.
- # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron.
- def log_error(exception) #:nodoc:
+ def log_error(exception)
logger = ActionView::Base.logger
return unless logger
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index f40bf8f6e2..54317199de 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -44,7 +44,7 @@ module ActionView
# Renders the given template. A string representing the layout can be
# supplied as well.
- def render_template(template, layout_name = nil, locals = nil) #:nodoc:
+ def render_template(template, layout_name = nil, locals = nil)
view, locals = @view, locals || {}
render_with_layout(layout_name, locals) do |layout|
@@ -54,7 +54,7 @@ module ActionView
end
end
- def render_with_layout(path, locals) #:nodoc:
+ def render_with_layout(path, locals)
layout = path && find_layout(path, locals.keys, [formats.first])
content = yield(layout)
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index b70e7239fc..cf18562c45 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -91,7 +91,7 @@ module ActionView
# Find and render a template based on the options given.
# :api: private
- def _render_template(options) #:nodoc:
+ def _render_template(options)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
context = view_context
@@ -104,7 +104,7 @@ module ActionView
end
# Assign the rendered format to look up context.
- def _process_format(format) #:nodoc:
+ def _process_format(format)
super
lookup_context.formats = [format.to_sym]
lookup_context.rendered_format = lookup_context.formats.first
@@ -124,7 +124,11 @@ module ActionView
key = action.include?(?/) ? :template : :action
options[key] = action
else
- options[:partial] = action
+ if action.respond_to?(:permitted?) && action.permitted?
+ options = action
+ else
+ options[:partial] = action
+ end
end
options
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index 669cffab1a..687ba7c1b4 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -122,18 +122,15 @@ module ActionView
controller.url_options
end
- def _routes_context #:nodoc:
- controller
- end
- protected :_routes_context
-
- def optimize_routes_generation? #:nodoc:
- controller.respond_to?(:optimize_routes_generation?, true) ?
- controller.optimize_routes_generation? : super
- end
- protected :optimize_routes_generation?
-
private
+ def _routes_context
+ controller
+ end
+
+ def optimize_routes_generation?
+ controller.respond_to?(:optimize_routes_generation?, true) ?
+ controller.optimize_routes_generation? : super
+ end
def _generate_paths_by_default
true
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 2dcd6324db..b0e2f1e54e 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -140,7 +140,7 @@ module ActionView
end
# Returns whether the underlying handler supports streaming. If so,
- # a streaming buffer *may* be passed when it start rendering.
+ # a streaming buffer *may* be passed when it starts rendering.
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
end
@@ -231,11 +231,11 @@ module ActionView
end
end
- protected
+ private
# Compile a template. This method ensures a template is compiled
# just once and removes the source after it is compiled.
- def compile!(view) #:nodoc:
+ def compile!(view)
return if @compiled
# Templates can be used concurrently in threaded environments
@@ -276,9 +276,8 @@ module ActionView
# encode the source into <tt>Encoding.default_internal</tt>.
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
- def compile(mod) #:nodoc:
+ def compile(mod)
encode!
- method_name = self.method_name
code = @handler.call(self)
# Make sure that the resulting String to be eval'd is in the
@@ -309,7 +308,7 @@ module ActionView
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
end
- def handle_render_error(view, e) #:nodoc:
+ def handle_render_error(view, e)
if e.is_a?(Template::Error)
e.sub_template_of(self)
raise e
@@ -323,17 +322,17 @@ module ActionView
end
end
- def locals_code #:nodoc:
+ def locals_code
# Only locals with valid variable names get set directly. Others will
# still be available in local_assigns.
- locals = @locals.to_set - Module::DELEGATION_RESERVED_METHOD_NAMES
- locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
+ locals = @locals - Module::RUBY_RESERVED_KEYWORDS
+ locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
# Double assign to suppress the dreaded 'assigned but unused variable' warning
locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
end
- def method_name #:nodoc:
+ def method_name
@method_name ||= begin
m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
m.tr!("-".freeze, "_".freeze)
@@ -341,16 +340,14 @@ module ActionView
end
end
- def identifier_method_name #:nodoc:
+ def identifier_method_name
inspect.tr("^a-z_".freeze, "_".freeze)
end
- def instrument(action, &block)
- ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, instrument_payload, &block)
+ def instrument(action, &block) # :doc:
+ ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
end
- private
-
def instrument_render_template(&block)
ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, instrument_payload, &block)
end
diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb
index e08a5b5db8..e99b921cb7 100644
--- a/actionview/lib/action_view/template/handlers/builder.rb
+++ b/actionview/lib/action_view/template/handlers/builder.rb
@@ -7,15 +7,15 @@ module ActionView
def call(template)
require_engine
- "xml = ::Builder::XmlMarkup.new(:indent => 2);" +
+ "xml = ::Builder::XmlMarkup.new(:indent => 2);" \
"self.output_buffer = xml.target!;" +
template.source +
";xml.target!;"
end
- protected
+ private
- def require_engine
+ def require_engine # :doc:
@required ||= begin
require "builder"
true
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 5d047a6991..58c7fd1a88 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -1,79 +1,12 @@
-require "erubis"
-
module ActionView
class Template
module Handlers
- class Erubis < ::Erubis::Eruby
- 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
+ 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.
class_attribute :erb_trim_mode
@@ -81,7 +14,7 @@ module ActionView
# Default implementation used.
class_attribute :erb_implementation
- self.erb_implementation = Erubis
+ self.erb_implementation = Erubi
# Do not escape templates of these mime types.
class_attribute :escape_whitelist
diff --git a/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb
new file mode 100644
index 0000000000..427ea20064
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb
@@ -0,0 +1,9 @@
+::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/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
new file mode 100644
index 0000000000..755cc84015
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -0,0 +1,81 @@
+require "erubi"
+
+module ActionView
+ class Template
+ module Handlers
+ class ERB
+ class Erubi < ::Erubi::Engine
+ # :nodoc: all
+ def initialize(input, properties = {})
+ @newline_pending = 0
+
+ # Dup properties so that we don't modify argument
+ properties = Hash[properties]
+ properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
+ properties[:postamble] = "@output_buffer.to_s"
+ properties[:bufvar] = "@output_buffer"
+ properties[:escapefunc] = ""
+
+ super
+ end
+
+ def evaluate(action_view_erb_handler_context)
+ pr = eval("proc { #{@src} }", binding, @filename || "(erubi)")
+ action_view_erb_handler_context.instance_eval(&pr)
+ end
+
+ private
+ def add_text(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 << text.gsub(/['\\]/, '\\\\\&')
+ src << "'.freeze;"
+
+ @newline_pending = 0
+ end
+ end
+
+ BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
+
+ def add_expression(indicator, code)
+ flush_newline_if_pending(src)
+
+ if (indicator == "==") || @escape
+ src << "@output_buffer.safe_expr_append="
+ else
+ src << "@output_buffer.append="
+ end
+
+ if BLOCK_EXPR.match?(code)
+ src << " " << code
+ else
+ src << "(" << code << ");"
+ end
+ end
+
+ def add_code(code)
+ flush_newline_if_pending(src)
+ super
+ end
+
+ def add_postamble(_)
+ flush_newline_if_pending(src)
+ super
+ 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/handlers/erb/erubis.rb b/actionview/lib/action_view/template/handlers/erb/erubis.rb
new file mode 100644
index 0000000000..f3c35e1aec
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/erb/erubis.rb
@@ -0,0 +1,81 @@
+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 ed93ebc027..d3905b5f23 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -164,8 +164,8 @@ module ActionView
# This is what child classes implement. No defaults are needed
# because Resolver guarantees that the arguments are present and
# normalized.
- def find_templates(name, prefix, partial, details)
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
+ def find_templates(name, prefix, partial, details, outside_app_allowed = false)
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method"
end
# Helpers that builds a path. Useful for building virtual paths.
@@ -177,7 +177,7 @@ module ActionView
# always check the cache before hitting the resolver. Otherwise,
# it always hits the resolver but if the key is present, check if the
# resolver is fresher before returning it.
- def cached(key, path_info, details, locals) #:nodoc:
+ def cached(key, path_info, details, locals)
name, prefix, partial = path_info
locals = locals.map(&:to_s).sort!
@@ -191,7 +191,7 @@ module ActionView
end
# Ensures all the resolver information is set in the template.
- def decorate(templates, path_info, details, locals) #:nodoc:
+ def decorate(templates, path_info, details, locals)
cached = nil
templates.each do |t|
t.locals = locals
@@ -226,7 +226,7 @@ module ActionView
template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
template_paths.map do |template|
- handler, format, variant = extract_handler_and_format_and_variant(template, formats)
+ handler, format, variant = extract_handler_and_format_and_variant(template)
contents = File.binread(template)
Template.new(contents, File.expand_path(template), handler,
@@ -289,7 +289,7 @@ module ActionView
# Extract handler, formats and variant from path. If a format cannot be found neither
# from the path, or the handler, we should return the array of formats given
# to the resolver.
- def extract_handler_and_format_and_variant(path, default_formats)
+ def extract_handler_and_format_and_variant(path)
pieces = File.basename(path).split(".".freeze)
pieces.shift
diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index e8d4e18f04..380528d6ef 100644
--- a/actionview/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
@@ -4,10 +4,9 @@ module ActionView #:nodoc:
class Text #:nodoc:
attr_accessor :type
- def initialize(string, type = nil)
+ def initialize(string)
@string = string.to_s
- @type = Types[type] || type if type
- @type ||= Types[:text]
+ @type = Types[:text]
end
def identifier
@@ -25,7 +24,7 @@ module ActionView #:nodoc:
end
def formats
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ [@type.ref]
end
end
end
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index 5fb7bb54b5..ae4fec4337 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -124,6 +124,10 @@ module ActionView
@_rendered_views ||= RenderedViewsCollection.new
end
+ def _routes
+ @controller._routes if @controller.respond_to?(:_routes)
+ end
+
# Need to experiment if this priority is the best one: rendered => output_buffer
class RenderedViewsCollection
def initialize
@@ -206,8 +210,8 @@ module ActionView
view = @controller.view_context
view.singleton_class.include(_helpers)
view.extend(Locals)
- view.rendered_views = self.rendered_views
- view.output_buffer = self.output_buffer
+ view.rendered_views = rendered_views
+ view.output_buffer = output_buffer
view
end
end
@@ -258,10 +262,6 @@ module ActionView
end]
end
- def _routes
- @controller._routes if @controller.respond_to?(:_routes)
- end
-
def method_missing(selector, *args)
begin
routes = @controller.respond_to?(:_routes) && @controller._routes
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index f4a7a9138c..3188526b63 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -17,35 +17,35 @@ module ActionView #:nodoc:
@hash.keys.join(", ")
end
- private
-
- def query(path, exts, formats, _)
- query = ""
- EXTENSIONS.each_key do |ext|
- query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)"
- end
- query = /^(#{Regexp.escape(path)})#{query}$/
-
- templates = []
- @hash.each do |_path, array|
- source, updated_at = array
- next unless query.match?(_path)
- handler, format, variant = extract_handler_and_format_and_variant(_path, formats)
- templates << Template.new(source, _path, handler,
- virtual_path: path.virtual,
- format: format,
- variant: variant,
- updated_at: updated_at
- )
+ private
+
+ def query(path, exts, _, _)
+ query = ""
+ EXTENSIONS.each_key do |ext|
+ query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)"
+ end
+ query = /^(#{Regexp.escape(path)})#{query}$/
+
+ templates = []
+ @hash.each do |_path, array|
+ source, updated_at = array
+ next unless query.match?(_path)
+ handler, format, variant = extract_handler_and_format_and_variant(_path)
+ templates << Template.new(source, _path, handler,
+ virtual_path: path.virtual,
+ format: format,
+ variant: variant,
+ updated_at: updated_at
+ )
+ end
+
+ templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
end
-
- templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
- end
end
class NullResolver < PathResolver
- def query(path, exts, formats, _)
- handler, format, variant = extract_handler_and_format_and_variant(path, formats)
+ def query(path, exts, _, _)
+ handler, format, variant = extract_handler_and_format_and_variant(path)
[ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant)]
end
end
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index a9638d1e6d..f0fe6831fa 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -5,7 +5,7 @@ module ActionView
included do
class_attribute :_view_paths
self._view_paths = ActionView::PathSet.new
- self._view_paths.freeze
+ _view_paths.freeze
end
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
diff --git a/actionview/package.json b/actionview/package.json
new file mode 100644
index 0000000000..690749a051
--- /dev/null
+++ b/actionview/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "rails-ujs",
+ "version": "5.2.0-alpha",
+ "description": "Ruby on Rails unobtrusive scripting adapter",
+ "main": "lib/assets/compiled/rails-ujs.js",
+ "files": [
+ "lib/assets/compiled/*.js"
+ ],
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "build": "bundle exec blade build",
+ "test": "echo \"See the README: https://github.com/rails/rails-ujs#how-to-run-tests\" && exit 1",
+ "lint": "coffeelint app/assets/javascripts && eslint test/public/test"
+ },
+ "repository": {
+ "type": "git",
+ "url": "rails/rails"
+ },
+ "contributors": [
+ "Stephen St. Martin",
+ "Steve Schwartz",
+ "Dangyi Liu",
+ "All contributors"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/rails/rails/issues"
+ },
+ "homepage": "http://rubyonrails.org/",
+ "devDependencies": {
+ "coffeelint": "^1.15.7",
+ "eslint": "^2.13.1"
+ }
+}
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 88c7189d22..dde66a7ba0 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -9,8 +9,8 @@ require "active_support/core_ext/kernel/reporting"
# These are the normal settings that will be set up by Railties
# TODO: Have these tests support other combinations of these values
silence_warnings do
- Encoding.default_internal = "UTF-8"
- Encoding.default_external = "UTF-8"
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
end
require "active_support/testing/autorun"
@@ -163,7 +163,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
# 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
- protected
+ private
def controller_reference(controller_param)
controller_param
end
@@ -271,15 +271,15 @@ module ActionDispatch
end
end
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
-end
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
-end
-
class ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ private def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ private def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb
index 5a2f0839e5..83237518d7 100644
--- a/actionview/test/actionpack/abstract/helper_test.rb
+++ b/actionview/test/actionpack/abstract/helper_test.rb
@@ -109,7 +109,7 @@ module AbstractController
class InvalidHelpersTest < ActiveSupport::TestCase
def test_controller_raise_error_about_real_require_problem
e = assert_raise(LoadError) { AbstractInvalidHelpers.helper(:invalid_require) }
- assert_equal "No such file to load -- very_invalid_file_name", e.message
+ assert_equal "No such file to load -- very_invalid_file_name.rb", e.message
end
def test_controller_raise_error_about_missing_helper
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index a342b22161..b79835ff34 100644
--- a/actionview/test/actionpack/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -252,7 +252,7 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase
end
end
-unless /mswin|mingw/.match?(RbConfig::CONFIG["host_os"])
+unless Gem.win_platform?
class LayoutSymlinkedTest < LayoutTest
layout "symlinked/symlinked_layout"
end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index b4a757d6b8..51ec8899b1 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -1,45 +1,14 @@
require "abstract_unit"
require "active_model"
+require "controller/fake_models"
class ApplicationController < ActionController::Base
self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
end
-class Customer < Struct.new(:name, :id)
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- undef_method :to_json
-
- def to_xml(options = {})
- if options[:builder]
- options[:builder].name name
- else
- "<name>#{name}</name>"
- end
- end
-
- def to_js(options = {})
- "name: #{name.inspect}"
- end
- alias :to_text :to_js
-
- def errors
- []
- end
-
- def persisted?
- id.present?
- end
-
- def cache_key
- name.to_s
- end
-end
-
module Quiz
#Models
- class Question < Struct.new(:name, :id)
+ Question = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -56,9 +25,6 @@ module Quiz
end
end
-class BadCustomer < Customer; end
-class GoodCustomer < Customer; end
-
module Fun
class GamesController < ApplicationController
def hello_world; end
diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb
index 6152ec4720..3b314588c7 100644
--- a/actionview/test/activerecord/form_helper_activerecord_test.rb
+++ b/actionview/test/activerecord/form_helper_activerecord_test.rb
@@ -45,14 +45,14 @@ class FormHelperActiveRecordTest < ActionView::TestCase
end
expected = whole_form("/developers/123", "edit_developer_123", "edit_developer", method: "patch") do
- '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' +
+ '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' \
'<input id="developer_projects_attributes_abc_id" name="developer[projects_attributes][abc][id]" type="hidden" value="321" />'
end
assert_dom_equal expected, output_buffer
end
- protected
+ private
def hidden_fields(method = nil)
txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
index 8495949975..b2e0fb08c4 100644
--- a/actionview/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -45,7 +45,7 @@ class ModelDelegate
end
end
-module Blog
+module Weblog
class Post < ActiveRecord::Base
self.table_name = "projects"
end
@@ -61,7 +61,7 @@ end
class PolymorphicRoutesTest < ActionController::TestCase
include SharedTestRoutes.url_helpers
- self.default_url_options[:host] = "example.com"
+ default_url_options[:host] = "example.com"
def setup
@project = Project.new
@@ -72,8 +72,8 @@ class PolymorphicRoutesTest < ActionController::TestCase
@fax = Fax.new
@delegator = ModelDelegator.new
@series = Series.new
- @blog_post = Blog::Post.new
- @blog_blog = Blog::Blog.new
+ @blog_post = Weblog::Post.new
+ @blog_blog = Weblog::Blog.new
end
def assert_url(url, args)
@@ -86,8 +86,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_string
with_test_routes do
- # FIXME: why are these different? Symbol case passes through to
- # `polymorphic_url`, but the String case doesn't.
+ assert_equal "/projects", polymorphic_path("projects")
assert_equal "http://example.com/projects", polymorphic_url("projects")
assert_equal "projects", url_for("projects")
end
@@ -731,3 +730,49 @@ class PolymorphicPathRoutesTest < PolymorphicRoutesTest
assert_equal url.sub(/http:\/\/#{host}/, ""), url_for(args)
end
end
+
+class DirectRoutesTest < ActionView::TestCase
+ class Linkable
+ attr_reader :id
+
+ def self.name
+ super.demodulize
+ end
+
+ def initialize(id)
+ @id = id
+ end
+
+ def linkable_type
+ self.class.name.underscore
+ end
+ end
+
+ class Category < Linkable; end
+ class Collection < Linkable; end
+ class Product < Linkable; end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ resources :categories, :collections, :products
+ direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] }
+ end
+
+ include Routes.url_helpers
+
+ def setup
+ @category = Category.new("1")
+ @collection = Collection.new("2")
+ @product = Product.new("3")
+ end
+
+ def test_direct_routes
+ assert_equal "/categories/1", linkable_path(@category)
+ assert_equal "/collections/2", linkable_path(@collection)
+ assert_equal "/products/3", linkable_path(@product)
+
+ assert_equal "http://test.host/categories/1", linkable_url(@category)
+ assert_equal "http://test.host/collections/2", linkable_url(@collection)
+ assert_equal "http://test.host/products/3", linkable_url(@product)
+ 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 55886da30f..60c3ab3045 100644
--- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb
+++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
@@ -87,7 +87,7 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase
end
end
-class Game < Struct.new(:name, :id)
+Game = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
def to_param
diff --git a/actionview/test/fixtures/test/_partial_iteration_1.erb b/actionview/test/fixtures/test/_partial_iteration_1.erb
new file mode 100644
index 0000000000..c0fdd4c22a
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_iteration_1.erb
@@ -0,0 +1 @@
+<%= defined?(partial_iteration_1_iteration) %>
diff --git a/actionview/test/fixtures/test/_partial_iteration_2.erb b/actionview/test/fixtures/test/_partial_iteration_2.erb
new file mode 100644
index 0000000000..50dd11db27
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_iteration_2.erb
@@ -0,0 +1 @@
+<%= defined?(partial_iteration_2_iteration) -%>
diff --git a/actionview/test/fixtures/test/render_file_instance_variable.erb b/actionview/test/fixtures/test/render_file_instance_variable.erb
new file mode 100644
index 0000000000..5344ac8a66
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_instance_variable.erb
@@ -0,0 +1 @@
+<%= @foo %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb b/actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb
new file mode 100644
index 0000000000..edfe52e422
--- /dev/null
+++ b/actionview/test/fixtures/test/test_template_with_delegation_reserved_keywords.erb
@@ -0,0 +1 @@
+<%= _ %> <%= arg %> <%= args %> <%= block %> \ No newline at end of file
diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb
index 80649db88b..5250101220 100644
--- a/actionview/test/lib/controller/fake_models.rb
+++ b/actionview/test/lib/controller/fake_models.rb
@@ -1,6 +1,6 @@
require "active_model"
-class Customer < Struct.new(:name, :id)
+Customer = Struct.new(:name, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -26,12 +26,16 @@ class Customer < Struct.new(:name, :id)
def persisted?
id.present?
end
-end
-class GoodCustomer < Customer
+ def cache_key
+ name.to_s
+ end
end
-class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost)
+class BadCustomer < Customer; end
+class GoodCustomer < Customer; end
+
+Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do
extend ActiveModel::Naming
include ActiveModel::Conversion
extend ActiveModel::Translation
@@ -80,7 +84,7 @@ class Comment
def to_key; id ? [id] : nil end
def save; @id = 1; @post_id = 1 end
def persisted?; @id.present? end
- def to_param; @id.to_s; end
+ def to_param; @id && @id.to_s; end
def name
@id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
end
@@ -101,7 +105,7 @@ class Tag
def to_key; id ? [id] : nil end
def save; @id = 1; @post_id = 1 end
def persisted?; @id.present? end
- def to_param; @id; end
+ def to_param; @id && @id.to_s; end
def value
@id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
end
@@ -120,7 +124,7 @@ class CommentRelevance
def to_key; id ? [id] : nil end
def save; @id = 1; @comment_id = 1 end
def persisted?; @id.present? end
- def to_param; @id; end
+ def to_param; @id && @id.to_s; end
def value
@id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
end
@@ -136,7 +140,7 @@ class TagRelevance
def to_key; id ? [id] : nil end
def save; @id = 1; @tag_id = 1 end
def persisted?; @id.present? end
- def to_param; @id; end
+ def to_param; @id && @id.to_s; end
def value
@id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
end
@@ -163,7 +167,7 @@ module Blog
true
end
- class Post < Struct.new(:title, :id)
+ Post = Struct.new(:title, :id) do
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -183,8 +187,7 @@ class ArelLike
end
end
-class Car < Struct.new(:color)
-end
+Car = Struct.new(:color)
class Plane
attr_reader :to_key
diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb
index 8b8f686f96..6b63aa25a5 100644
--- a/actionview/test/template/active_model_helper_test.rb
+++ b/actionview/test/template/active_model_helper_test.rb
@@ -4,7 +4,7 @@ class ActiveModelHelperTest < ActionView::TestCase
tests ActionView::Helpers::ActiveModelHelper
silence_warnings do
- class Post < Struct.new(:author_name, :body, :updated_at)
+ Post = Struct.new(:author_name, :body, :updated_at) do
include ActiveModel::Conversion
include ActiveModel::Validations
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index 3bdab42f7a..07a6452cc1 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -630,7 +630,7 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
end
def test_should_return_nothing_if_asset_host_isnt_configured
- assert_equal nil, compute_asset_host("foo")
+ assert_nil compute_asset_host("foo")
end
def test_should_current_request_host_is_always_returned_for_request
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
index e9a923dd72..1245a1a966 100644
--- a/actionview/test/template/atom_feed_helper_test.rb
+++ b/actionview/test/template/atom_feed_helper_test.rb
@@ -1,6 +1,6 @@
require "abstract_unit"
-class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at)
+Scroll = Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) do
extend ActiveModel::Naming
include ActiveModel::Conversion
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
index 54bf9b4c33..7f37523eeb 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -127,18 +127,18 @@ class CaptureHelperTest < ActionView::TestCase
def test_content_for_returns_nil_when_writing
assert ! content_for?(:title)
- assert_equal nil, content_for(:title, "foo")
- assert_equal nil, content_for(:title) { output_buffer << "bar"; nil }
- assert_equal nil, content_for(:title) { output_buffer << " \n "; nil }
+ assert_nil content_for(:title, "foo")
+ assert_nil content_for(:title) { output_buffer << "bar"; nil }
+ assert_nil content_for(:title) { output_buffer << " \n "; nil }
assert_equal "foobar", content_for(:title)
- assert_equal nil, content_for(:title, "foo", flush: true)
- assert_equal nil, content_for(:title, flush: true) { output_buffer << "bar"; nil }
- assert_equal nil, content_for(:title, flush: true) { output_buffer << " \n "; nil }
+ assert_nil content_for(:title, "foo", flush: true)
+ assert_nil content_for(:title, flush: true) { output_buffer << "bar"; nil }
+ assert_nil content_for(:title, flush: true) { output_buffer << " \n "; nil }
assert_equal "bar", content_for(:title)
end
def test_content_for_returns_nil_when_content_missing
- assert_equal nil, content_for(:some_missing_key)
+ assert_nil content_for(:some_missing_key)
end
def test_content_for_question_mark
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index 3ecac46d34..adb2be9be4 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -24,10 +24,24 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
assert_equal locals.inspect, render(file: "test/render_file_inspect_local_assigns", locals: locals)
end
+ def test_template_with_delegation_reserved_keywords
+ locals = {
+ _: "one",
+ arg: "two",
+ args: "three",
+ block: "four",
+ }
+ assert_equal "one two three four", render(file: "test/test_template_with_delegation_reserved_keywords", locals: locals)
+ end
+
def test_template_with_unicode_identifier
assert_equal "🎂", render(file: "test/render_file_unicode_local", locals: { 🎃: "🎂" })
end
+ def test_template_with_instance_variable_identifier
+ assert_equal "bar", render(file: "test/render_file_instance_variable", locals: { "@foo": "bar" })
+ end
+
def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
assert_equal "one", render(file: "test/render_file_with_locals_and_default")
assert_equal "two", render(file: "test/render_file_with_locals_and_default", locals: { secret: "two" })
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index a84ac18bee..d257147e1f 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -832,7 +832,7 @@ class DateHelperTest < ActionView::TestCase
def test_select_date_with_too_big_range_between_start_year_and_end_year
assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), start_year: 2000, end_year: 20000, prefix: "date[first]", order: [:month, :day, :year]) }
- assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), start_year: Date.today.year - 100.years, end_year: 2000, prefix: "date[first]", order: [:month, :day, :year]) }
+ assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), start_year: 100, end_year: 2000, prefix: "date[first]", order: [:month, :day, :year]) }
end
def test_select_date_can_have_more_then_1000_years_interval_if_forced_via_parameter
@@ -1407,7 +1407,6 @@ class DateHelperTest < ActionView::TestCase
end
def test_select_datetime_with_custom_prompt
-
expected = %(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
expected << "</select>\n"
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index a814cab686..e225c3de09 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -122,13 +122,13 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_logging_of_missing_template_for_dependencies
- assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do
+ assert_logged "Couldn't find template for digesting: messages/something_missing" do
dependencies("messages/something_missing")
end
end
def test_logging_of_missing_template_for_nested_dependencies
- assert_logged "'messages/something_missing' file doesn't exist, so no dependencies" do
+ assert_logged "Couldn't find template for digesting: messages/something_missing" do
nested_dependencies("messages/something_missing")
end
end
diff --git a/actionview/test/template/erb/deprecated_erubis_implementation_test.rb b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb
new file mode 100644
index 0000000000..aaf99f85c0
--- /dev/null
+++ b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb
@@ -0,0 +1,13 @@
+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/helper.rb b/actionview/test/template/erb/helper.rb
index a1973068d5..bc1eedc15f 100644
--- a/actionview/test/template/erb/helper.rb
+++ b/actionview/test/template/erb/helper.rb
@@ -14,7 +14,7 @@ module ERBTest
class BlockTestCase < ActiveSupport::TestCase
def render_content(start, inside)
template = block_helper(start, inside)
- ActionView::Template::Handlers::Erubis.new(template).evaluate(ViewContext.new)
+ ActionView::Template::Handlers::ERB.erb_implementation.new(template).evaluate(ViewContext.new)
end
def block_helper(str, rest)
diff --git a/actionview/test/template/erb/tag_helper_test.rb b/actionview/test/template/erb/tag_helper_test.rb
index 84e328d8be..233f37c48a 100644
--- a/actionview/test/template/erb/tag_helper_test.rb
+++ b/actionview/test/template/erb/tag_helper_test.rb
@@ -18,8 +18,8 @@ module ERBTest
end
test "percent equals works with form tags" do
- expected_output = %r{<form.*action="foo".*method="post">.*hello*</form>}
- assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>")
+ expected_output = %r{<form.*action="/foo".*method="post">.*hello*</form>}
+ assert_match expected_output, render_content("form_tag('/foo')", "<%= 'hello' %>")
end
test "percent equals works with fieldset tags" do
diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb
index 0a16364ed3..4412ea37fa 100644
--- a/actionview/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
@@ -51,7 +51,12 @@ class ErbUtilTest < ActiveSupport::TestCase
def test_json_escape_does_not_alter_json_string_meaning
JSON_ESCAPE_TEST_CASES.each do |(raw, _)|
- assert_equal ActiveSupport::JSON.decode(raw), ActiveSupport::JSON.decode(json_escape(raw))
+ expected = ActiveSupport::JSON.decode(raw)
+ if expected.nil?
+ assert_nil ActiveSupport::JSON.decode(json_escape(raw))
+ else
+ assert_equal expected, ActiveSupport::JSON.decode(json_escape(raw))
+ end
end
end
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index 3774dcf872..6160524eb3 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -1,7 +1,6 @@
require "abstract_unit"
-class Category < Struct.new(:id, :name)
-end
+Category = Struct.new(:id, :name)
class FormCollectionsHelperTest < ActionView::TestCase
def assert_no_select(selector, value = nil)
diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb
new file mode 100644
index 0000000000..a4a2966ff9
--- /dev/null
+++ b/actionview/test/template/form_helper/form_with_test.rb
@@ -0,0 +1,2231 @@
+require "abstract_unit"
+require "controller/fake_models"
+
+class FormWithTest < ActionView::TestCase
+ include RenderERBUtils
+end
+
+class FormWithActsLikeFormTagTest < FormWithTest
+ tests ActionView::Helpers::FormTagHelper
+
+ setup do
+ @controller = BasicController.new
+ end
+
+ def hidden_fields(options = {})
+ method = options[:method]
+ skip_enforcing_utf8 = options.fetch(:skip_enforcing_utf8, false)
+
+ "".tap do |txt|
+ unless skip_enforcing_utf8
+ txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ end
+
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+ end
+ end
+
+ def form_text(action = "http://www.example.com", local: false, **options)
+ enctype, html_class, id, method = options.values_at(:enctype, :html_class, :id, :method)
+
+ method = method.to_s == "get" ? "get" : "post"
+
+ txt = %{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if enctype
+ txt << %{ data-remote="true"} unless local
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ txt << %{ method="#{method}">}
+ end
+
+ def whole_form(action = "http://www.example.com", options = {})
+ out = form_text(action, options) + hidden_fields(options)
+
+ if block_given?
+ out << yield << "</form>"
+ end
+
+ out
+ end
+
+ def url_for(options)
+ if options.is_a?(Hash)
+ "http://www.example.com"
+ else
+ super
+ end
+ end
+
+ def test_form_with_multipart
+ actual = form_with(multipart: true)
+
+ expected = whole_form("http://www.example.com", enctype: true)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_with_with_method_patch
+ actual = form_with(method: :patch)
+
+ expected = whole_form("http://www.example.com", method: :patch)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_with_with_method_put
+ actual = form_with(method: :put)
+
+ expected = whole_form("http://www.example.com", method: :put)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_with_with_method_delete
+ actual = form_with(method: :delete)
+
+ expected = whole_form("http://www.example.com", method: :delete)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_with_with_local_true
+ actual = form_with(local: true)
+
+ expected = whole_form("http://www.example.com", local: true)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_with_skip_enforcing_utf8_true
+ 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?
+ end
+
+ def test_form_with_with_block_in_erb
+ output_buffer = render_erb("<%= form_with(url: 'http://www.example.com') do %>Hello world!<% end %>")
+
+ expected = whole_form { "Hello world!" }
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_block_and_method_in_erb
+ output_buffer = render_erb("<%= form_with(url: 'http://www.example.com', method: :put) do %>Hello world!<% end %>")
+
+ expected = whole_form("http://www.example.com", method: "put") do
+ "Hello world!"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_block_in_erb_and_local_true
+ output_buffer = render_erb("<%= form_with(url: 'http://www.example.com', local: true) do %>Hello world!<% end %>")
+
+ expected = whole_form("http://www.example.com", local: true) do
+ "Hello world!"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+end
+
+class FormWithActsLikeFormForTest < FormWithTest
+ def form_with(*)
+ @output_buffer = super
+ end
+
+ teardown do
+ I18n.backend.reload!
+ end
+
+ setup do
+ # Create "label" locale for testing I18n label helpers
+ I18n.backend.store_translations "label",
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ },
+ "post/language": {
+ spanish: "Espanol"
+ }
+ }
+ },
+ helpers: {
+ label: {
+ post: {
+ body: "Write entire text here",
+ color: {
+ red: "Rojo"
+ },
+ comments: {
+ body: "Write body here"
+ }
+ },
+ tag: {
+ value: "Tag"
+ },
+ post_delegate: {
+ title: "Delegate model_name title"
+ }
+ }
+ }
+
+ # Create "submit" locale for testing I18n submit helpers
+ I18n.backend.store_translations "submit",
+ helpers: {
+ submit: {
+ create: "Create %{model}",
+ update: "Confirm %{model} changes",
+ submit: "Save changes",
+ another_post: {
+ update: "Update your %{model}"
+ }
+ }
+ }
+
+ I18n.backend.store_translations "placeholder",
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ },
+ "post/cost": {
+ uk: "Pounds"
+ }
+ }
+ },
+ helpers: {
+ placeholder: {
+ post: {
+ title: "What is this about?",
+ written_on: {
+ spanish: "Escrito en"
+ },
+ comments: {
+ body: "Write body here"
+ }
+ },
+ post_delegate: {
+ title: "Delegate model_name title"
+ },
+ tag: {
+ value: "Tag"
+ }
+ }
+ }
+
+ @post = Post.new
+ @comment = Comment.new
+ def @post.errors()
+ Class.new {
+ def [](field); field == "author_name" ? ["can't be empty"] : [] end
+ def empty?() false end
+ def count() 1 end
+ def full_messages() ["Author name can't be empty"] end
+ }.new
+ end
+ def @post.to_key; [123]; end
+ def @post.id; 0; end
+ def @post.id_before_type_cast; "omg"; end
+ def @post.id_came_from_user?; true; end
+ def @post.to_param; "123"; end
+
+ @post.persisted = true
+ @post.title = "Hello World"
+ @post.author_name = ""
+ @post.body = "Back to the hill and over it again!"
+ @post.secret = 1
+ @post.written_on = Date.new(2004, 6, 15)
+
+ @post.comments = []
+ @post.comments << @comment
+
+ @post.tags = []
+ @post.tags << Tag.new
+
+ @post_delegator = PostDelegator.new
+
+ @post_delegator.title = "Hello World"
+
+ @car = Car.new("#000FFF")
+ end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ resources :posts do
+ resources :comments
+ end
+
+ namespace :admin do
+ resources :posts do
+ resources :comments
+ end
+ end
+
+ get "/foo", to: "controller#action"
+ root to: "main#index"
+ end
+
+ def _routes
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def url_for(object)
+ @url_for_options = object
+
+ if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank?
+ object.merge!(controller: "main", action: "index")
+ end
+
+ super
+ end
+
+ def test_form_with_requires_arguments
+ error = assert_raises(ArgumentError) do
+ form_for(nil, html: { id: "create-post" }) do
+ end
+ end
+ assert_equal "First argument in form cannot contain nil or be empty", error.message
+
+ error = assert_raises(ArgumentError) do
+ form_for([nil, nil], html: { id: "create-post" }) do
+ end
+ end
+ assert_equal "First argument in form cannot contain nil or be empty", error.message
+ end
+
+ def test_form_with
+ 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.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", method: "patch") do
+ "<label for='post_title'>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' />" \
+ "<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_only_url_on_create
+ form_with(url: "/posts") do |f|
+ concat f.label :title, "Label me"
+ concat f.text_field :title
+ end
+
+ expected = whole_form("/posts") do
+ '<label for="title">Label me</label>' \
+ '<input type="text" name="title">'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_only_url_on_update
+ form_with(url: "/posts/123") do |f|
+ concat f.label :title, "Label me"
+ concat f.text_field :title
+ end
+
+ expected = whole_form("/posts/123") do
+ '<label for="title">Label me</label>' \
+ '<input type="text" name="title">'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_general_attributes
+ form_with(url: "/posts/123") do |f|
+ concat f.text_field :no_model_to_back_this_badboy
+ end
+
+ expected = whole_form("/posts/123") do
+ '<input type="text" name="no_model_to_back_this_badboy">'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_attribute_not_on_model
+ form_with(model: @post) do |f|
+ concat f.text_field :this_dont_exist_on_post
+ end
+
+ expected = whole_form("/posts/123", method: :patch) do
+ '<input type="text" name="post[this_dont_exist_on_post]">'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_doesnt_call_private_or_protected_properties_on_form_object_skipping_value
+ obj = Class.new do
+ private def private_property
+ "That would be great."
+ end
+
+ protected def protected_property
+ "I believe you have my stapler."
+ end
+ 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)
+ end
+ end
+
+ def test_form_with_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+ form_with(model: post) do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts") do
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<input name='post[active]' type='radio' value='true' />" \
+ "<label for='post_active_true'>true</label>" \
+ "<input checked='checked' name='post[active]' type='radio' value='false' />" \
+ "<label for='post_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_collection_radio_buttons_with_custom_builder_block
+ post = Post.new
+ def post.active; false; end
+
+ form_with(model: post) do |f|
+ rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b|
+ b.label { b.radio_button + b.text }
+ end
+ concat rendered_radio_buttons
+ end
+
+ expected = whole_form("/posts") do
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<label for='post_active_true'>" \
+ "<input name='post[active]' type='radio' value='true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' name='post[active]' type='radio' value='false' />" \
+ "false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template
+ post = Post.new
+ def post.active; false; end
+ def post.id; 1; end
+
+ form_with(model: post) do |f|
+ rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b|
+ b.label { b.radio_button + b.text }
+ end
+ concat rendered_radio_buttons
+ concat f.hidden_field :id
+ end
+
+ expected = whole_form("/posts") do
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<label for='post_active_true'>" \
+ "<input name='post[active]' type='radio' value='true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' name='post[active]' type='radio' value='false' />" \
+ "false</label>" \
+ "<input name='post[id]' type='hidden' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_index_and_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+
+ form_with(model: post, index: "1") do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts") do
+ "<input type='hidden' name='post[1][active]' value='' />" \
+ "<input name='post[1][active]' type='radio' value='true' />" \
+ "<label for='post_1_active_true'>true</label>" \
+ "<input checked='checked' name='post[1][active]' type='radio' value='false' />" \
+ "<label for='post_1_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ collection = (1..3).map { |i| [i, "Tag #{i}"] }
+ form_with(model: post) do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts") do
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "<label for='post_tag_ids_1'>Tag 1</label>" \
+ "<input name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "<label for='post_tag_ids_2'>Tag 2</label>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \
+ "<label for='post_tag_ids_3'>Tag 3</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_collection_check_boxes_with_custom_builder_block
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ collection = (1..3).map { |i| [i, "Tag #{i}"] }
+ form_with(model: post) do |f|
+ rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b|
+ b.label { b.check_box + b.text }
+ end
+ concat rendered_check_boxes
+ end
+
+ expected = whole_form("/posts") do
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<label for='post_tag_ids_1'>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \
+ "Tag 3</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ def post.id; 1; end
+ collection = (1..3).map { |i| [i, "Tag #{i}"] }
+
+ form_with(model: post) do |f|
+ rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b|
+ b.label { b.check_box + b.text }
+ end
+ concat rendered_check_boxes
+ concat f.hidden_field :id
+ end
+
+ expected = whole_form("/posts") do
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<label for='post_tag_ids_1'>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \
+ "Tag 3</label>" \
+ "<input name='post[id]' type='hidden' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_index_and_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1]; end
+ collection = [[1, "Tag 1"]]
+
+ form_with(model: post, index: "1") do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts") do
+ "<input name='post[1][tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' />" \
+ "<label for='post_1_tag_ids_1'>Tag 1</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_file_field_generate_multipart
+ form_with(model: @post, id: "create-post") do |f|
+ concat f.file_field(:file)
+ end
+
+ expected = whole_form("/posts/123", "create-post", method: "patch", multipart: true) do
+ "<input name='post[file]' type='file' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_file_field_generate_multipart
+ form_with(model: @post) do |f|
+ concat f.fields(:comment, model: @post) { |c|
+ concat c.file_field(:file)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch", multipart: true) do
+ "<input name='post[comment][file]' type='file' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_format
+ form_with(model: @post, format: :json, id: "edit_post_123", class: "edit_post") do |f|
+ concat f.label(:title)
+ end
+
+ expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: "patch") do
+ "<label for='post_title'>Title</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_format_and_url
+ form_with(model: @post, format: :json, url: "/") do |f|
+ concat f.label(:title)
+ end
+
+ expected = whole_form("/", method: "patch") do
+ "<label for='post_title'>Title</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_model_using_relative_model_naming
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+
+ form_with(model: blog_post) do |f|
+ concat f.text_field :title
+ concat f.submit("Edit post")
+ 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='commit' data-disable-with='Edit post' type='submit' value='Edit post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_symbol_scope
+ form_with(model: @post, scope: "other_name", id: "create-post") do |f|
+ concat f.label(:title, class: "post_title")
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.submit("Create post")
+ end
+
+ 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[secret]' value='0' type='hidden' />" \
+ "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' />" \
+ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_method_as_part_of_html_options
+ form_with(model: @post, url: "/", id: "create-post", html: { method: :delete }) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_method
+ form_with(model: @post, url: "/", method: :delete, id: "create-post") do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_search_field
+ # Test case for bug which would emit an "object" attribute
+ # when used with form_for using a search_field form helper
+ form_with(model: Post.new, url: "/search", id: "search-post", method: :get) do |f|
+ concat f.search_field(:title)
+ end
+
+ expected = whole_form("/search", "search-post", method: "get") do
+ "<input name='post[title]' type='search' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_enables_remote_by_default
+ form_with(model: @post, url: "/", id: "create-post", method: :patch) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_skip_enforcing_utf8_true
+ form_with(scope: :post, skip_enforcing_utf8: true) 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' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_skip_enforcing_utf8_false
+ form_with(scope: :post, skip_enforcing_utf8: false) 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' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_without_object
+ form_with(scope: :post, id: "create-post") do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_index
+ form_with(model: @post, scope: "post[]") do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ 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][secret]' type='hidden' value='0' />" \
+ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_nil_index_option_override
+ form_with(model: @post, scope: "post[]", index: nil) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[][secret]' type='hidden' value='0' />" \
+ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_label_error_wrapping
+ form_with(model: @post) do |f|
+ concat f.label(:author_name, class: "label")
+ concat f.text_field(:author_name)
+ concat f.submit("Create post")
+ end
+
+ 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>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_label_error_wrapping_without_conventional_instance_variable
+ post = remove_instance_variable :@post
+
+ form_with(model: post) do |f|
+ concat f.label(:author_name, class: "label")
+ concat f.text_field(:author_name)
+ concat f.submit("Create post")
+ end
+
+ 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>" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_label_error_wrapping_block_and_non_block_versions
+ form_with(model: @post) do |f|
+ concat f.label(:author_name, "Name", class: "label")
+ concat f.label(:author_name, class: "label") { "Name" }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" \
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_namespace
+ skip "Do namespaces still make sense?"
+ form_for(@post, namespace: "namespace") do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_submit_with_object_as_new_record_and_locale_strings
+ with_locale :submit do
+ @post.persisted = false
+ @post.stub(:to_key, nil) do
+ form_with(model: @post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts") do
+ "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+ end
+
+ def test_submit_with_object_as_existing_record_and_locale_strings
+ with_locale :submit do
+ form_with(model: @post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_submit_without_object_and_locale_strings
+ with_locale :submit do
+ form_with(scope: :post) do |f|
+ concat f.submit class: "extra"
+ end
+
+ expected = whole_form do
+ "<input name='commit' class='extra' data-disable-with='Save changes' type='submit' value='Save changes' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_submit_with_object_and_nested_lookup
+ with_locale :submit do
+ form_with(model: @post, scope: :another_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/123", 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|
+ concat c.text_field :dont_exist_on_model
+ }
+ end
+
+ expected = whole_form("/posts/123", method: :patch) do
+ '<input type="text" name="post[comment][dont_exist_on_model]">'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_attributes_not_on_model_deep_nested
+ @comment.save
+ form_with(scope: :posts) do |f|
+ f.fields("post[]", model: @post) do |f2|
+ f2.text_field(:id)
+ @post.comments.each do |comment|
+ concat f2.fields("comment[]", model: comment) { |c|
+ concat c.text_field(:dont_exist_on_model)
+ }
+ end
+ end
+ end
+
+ expected = whole_form do
+ '<input name="posts[post][0][comment][1][dont_exist_on_model]" type="text">'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields
+ @comment.body = "Hello World"
+ form_with(model: @post) do |f|
+ concat f.fields(model: @comment) { |c|
+ concat c.text_field(:body)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[comment][body]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_deep_nested_fields
+ @comment.save
+ form_with(scope: :posts) do |f|
+ f.fields("post[]", model: @post) do |f2|
+ f2.text_field(:id)
+ @post.comments.each do |comment|
+ concat f2.fields("comment[]", model: comment) { |c|
+ concat c.text_field(:name)
+ }
+ end
+ end
+ end
+
+ expected = whole_form do
+ "<input name='posts[post][0][comment][1][name]' type='text' value='comment #1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_nested_collections
+ form_with(model: @post, scope: "post[]") do |f|
+ concat f.text_field(:title)
+ concat f.fields("comment[]", model: @comment) { |c|
+ concat c.text_field(:name)
+ }
+ 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' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_index_and_parent_fields
+ form_with(model: @post, index: 1) do |c|
+ concat c.text_field(:title)
+ concat c.fields("comment", model: @comment, index: 1) { |r|
+ concat r.text_field(:name)
+ }
+ 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' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_index_and_nested_fields
+ output_buffer = form_with(model: @post, index: 1) do |f|
+ concat f.fields(:comment, model: @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[1][comment][title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_index_on_both
+ form_with(model: @post, index: 1) do |f|
+ concat f.fields(:comment, model: @post, index: 5) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[1][comment][5][title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_auto_index
+ form_with(model: @post, scope: "post[]") do |f|
+ concat f.fields(:comment, model: @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[123][comment][title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_index_radio_button
+ form_with(model: @post) do |f|
+ concat f.fields(:comment, model: @post, index: 5) { |c|
+ concat c.radio_button(:title, "hello")
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[comment][5][title]' type='radio' value='hello' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_auto_index_on_both
+ form_with(model: @post, scope: "post[]") do |f|
+ concat f.fields("comment[]", model: @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[123][comment][123][title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_index_and_auto_index
+ output_buffer = form_with(model: @post, scope: "post[]") do |f|
+ concat f.fields(:comment, model: @post, index: 5) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ output_buffer << form_with(model: @post, index: 1) do |f|
+ concat f.fields("comment[]", model: @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[123][comment][5][title]' type='text' value='Hello World' />"
+ end + whole_form("/posts/123", method: "patch") do
+ "<input name='post[1][comment][123][title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_a_new_record_on_a_nested_attributes_one_to_one_association
+ @post.author = Author.new
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ concat af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association
+ form_with(model: @post) do |f|
+ f.fields(:author, model: Author.new(123)) do |af|
+ assert_not_nil af.object
+ assert_equal 123, af.object.id
+ end
+ end
+ end
+
+ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association
+ @post.author = Author.new(321)
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ concat af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block
+ @post.author = Author.new(321)
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id
+ @post.author = Author.new(321)
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author, skip_id: true) { |af|
+ af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited
+ @post.author = Author.new(321)
+
+ form_with(model: @post, skip_id: true) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override
+ @post.author = Author.new(321)
+
+ form_with(model: @post, skip_id: true) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author, skip_id: false) { |af|
+ af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement
+ @post.author = Author.new(321)
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ concat af.hidden_field(:id)
+ concat af.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment, skip_id: true) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_with(model: @post, skip_id: true) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_with(model: @post, skip_id: true) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:author, skip_id: false) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ concat cf.hidden_field(:id)
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_new_records_on_a_nested_attributes_collection_association
+ @post.comments = [Comment.new, Comment.new]
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_and_new_records_on_a_nested_attributes_collection_association
+ @post.comments = [Comment.new(321), Comment.new]
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields(:comments, model: comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_an_empty_supplied_attributes_collection
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ f.fields(:comments, model: []) do |cf|
+ concat cf.text_field(:name)
+ end
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ '<input name="post[title]" type="text" value="Hello World" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_existing_records_on_a_supplied_nested_attributes_collection
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:comments, model: @post.comments) { |cf|
+ concat cf.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_arel_like
+ @post.comments = ArelLike.new
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:comments, model: @post.comments) { |cf|
+ concat cf.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_label_translation_with_more_than_10_records
+ @post.comments = Array.new(11) { |id| Comment.new(id + 1) }
+
+ params = 11.times.map { ["post.comments.body", default: [:"comment.body", ""], scope: "helpers.label"] }
+ assert_called_with(I18n, :t, params, returns: "Write body here") do
+ form_with(model: @post) do |f|
+ f.fields(:comments) do |cf|
+ concat cf.label(:body)
+ end
+ end
+ end
+ end
+
+ def test_nested_fields_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one
+ comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.comments = []
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:comments, model: comments) { |cf|
+ concat cf.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_on_a_nested_attributes_collection_association_yields_only_builder
+ @post.comments = [Comment.new(321), Comment.new]
+ yielded_comments = []
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.fields(:comments) { |cf|
+ concat cf.text_field(:name)
+ yielded_comments << cf.object
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ assert_equal yielded_comments, @post.comments
+ end
+
+ def test_nested_fields_with_child_index_option_override_on_a_nested_attributes_collection_association
+ @post.comments = []
+
+ form_with(model: @post) do |f|
+ concat f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf|
+ concat cf.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association
+ @post.comments = []
+
+ form_with(model: @post) do |f|
+ concat f.fields(:comments, model: Comment.new(321), child_index: -> { "abc" }) { |cf|
+ concat cf.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ class FakeAssociationProxy
+ def to_ary
+ [1, 2, 3]
+ end
+ end
+
+ def test_nested_fields_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy
+ @post.comments = FakeAssociationProxy.new
+
+ form_with(model: @post) do |f|
+ concat f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf|
+ concat cf.text_field(:name)
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_index_method_with_existing_records_on_a_nested_attributes_collection_association
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_with(model: @post) do |f|
+ expected = 0
+ @post.comments.each do |comment|
+ f.fields(:comments, model: comment) { |cf|
+ assert_equal cf.index, expected
+ expected += 1
+ }
+ end
+ end
+ end
+
+ def test_nested_fields_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association
+ @post.comments = [Comment.new(321), Comment.new]
+
+ form_with(model: @post) do |f|
+ expected = 0
+ @post.comments.each do |comment|
+ f.fields(:comments, model: comment) { |cf|
+ assert_equal cf.index, expected
+ expected += 1
+ }
+ end
+ end
+ end
+
+ def test_nested_fields_index_method_with_existing_records_on_a_supplied_nested_attributes_collection
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_with(model: @post) do |f|
+ expected = 0
+ f.fields(:comments, model: @post.comments) { |cf|
+ assert_equal cf.index, expected
+ expected += 1
+ }
+ end
+ end
+
+ def test_nested_fields_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association
+ @post.comments = []
+
+ form_with(model: @post) do |f|
+ f.fields(:comments, model: Comment.new(321), child_index: "abc") { |cf|
+ assert_equal cf.index, "abc"
+ }
+ end
+ end
+
+ def test_nested_fields_uses_unique_indices_for_different_collection_associations
+ @post.comments = [Comment.new(321)]
+ @post.tags = [Tag.new(123), Tag.new(456)]
+ @post.comments[0].relevances = []
+ @post.tags[0].relevances = []
+ @post.tags[1].relevances = []
+
+ form_with(model: @post) do |f|
+ concat f.fields(:comments, model: @post.comments[0]) { |cf|
+ concat cf.text_field(:name)
+ concat cf.fields(:relevances, model: CommentRelevance.new(314)) { |crf|
+ concat crf.text_field(:value)
+ }
+ }
+ concat f.fields(:tags, model: @post.tags[0]) { |tf|
+ concat tf.text_field(:value)
+ concat tf.fields(:relevances, model: TagRelevance.new(3141)) { |trf|
+ concat trf.text_field(:value)
+ }
+ }
+ concat f.fields("tags", model: @post.tags[1]) { |tf|
+ concat tf.text_field(:value)
+ concat tf.fields(:relevances, model: TagRelevance.new(31415)) { |trf|
+ concat trf.text_field(:value)
+ }
+ }
+ 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" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_with_hash_like_model
+ @author = HashBackedAuthor.new
+
+ form_with(model: @post) do |f|
+ concat f.fields(:author, model: @author) { |af|
+ concat af.text_field(:name)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ '<input name="post[author_attributes][name]" type="text" value="hash backed author" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields
+ output_buffer = fields(:post, model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_index
+ output_buffer = fields("post[]", model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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][secret]' type='hidden' value='0' />" \
+ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_nil_index_option_override
+ output_buffer = fields("post[]", model: @post, index: nil) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[][secret]' type='hidden' value='0' />" \
+ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_index_option_override
+ output_buffer = fields("post[]", model: @post, index: "abc") do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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][secret]' type='hidden' value='0' />" \
+ "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_without_object
+ output_buffer = fields(:post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_only_object
+ output_buffer = fields(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_object_with_bracketed_name
+ output_buffer = fields("author[post]", model: @post) do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ assert_dom_equal "<label for=\"author_post_title\">Title</label>" \
+ "<input name='author[post][title]' type='text' value='Hello World' />",
+ output_buffer
+ end
+
+ def test_fields_object_with_bracketed_name_and_index
+ output_buffer = fields("author[post]", model: @post, index: 1) do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \
+ "<input name='author[post][1][title]' type='text' value='Hello World' />",
+ output_buffer
+ end
+
+ def test_form_builder_does_not_have_form_with_method
+ assert_not_includes ActionView::Helpers::FormBuilder.instance_methods, :form_with
+ end
+
+ def test_form_with_and_fields
+ form_with(model: @post, scope: :post, id: "create-post") do |post_form|
+ concat post_form.text_field(:title)
+ concat post_form.text_area(:body)
+
+ concat fields(:parent_post, model: @post) { |parent_fields|
+ concat parent_fields.check_box(:secret)
+ }
+ 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='parent_post[secret]' type='hidden' value='0' />" \
+ "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_and_fields_with_object
+ form_with(model: @post, scope: :post, id: "create-post") do |post_form|
+ concat post_form.text_field(:title)
+ concat post_form.text_area(:body)
+
+ concat post_form.fields(model: @comment) { |comment_fields|
+ concat comment_fields.text_field(:name)
+ }
+ 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' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_and_fields_with_non_nested_association_and_without_object
+ form_with(model: @post) do |f|
+ concat f.fields(:category) { |c|
+ concat c.text_field(:name)
+ }
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<input name='post[category][name]' type='text' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ class LabelledFormBuilder < ActionView::Helpers::FormBuilder
+ (field_helpers - %w(hidden_field)).each do |selector|
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def #{selector}(field, *args, &proc)
+ ("<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>").html_safe
+ end
+ RUBY_EVAL
+ end
+ end
+
+ def test_form_with_with_labelled_builder
+ form_with(model: @post, builder: LabelledFormBuilder) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_default_form_builder
+ old_default_form_builder, ActionView::Base.default_form_builder =
+ ActionView::Base.default_form_builder, LabelledFormBuilder
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Base.default_form_builder = old_default_form_builder
+ end
+
+ def test_lazy_loading_default_form_builder
+ old_default_form_builder, ActionView::Base.default_form_builder =
+ ActionView::Base.default_form_builder, "FormWithActsLikeFormForTest::LabelledFormBuilder"
+
+ form_with(model: @post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/posts/123", method: "patch") do
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Base.default_form_builder = old_default_form_builder
+ end
+
+ def test_form_builder_override
+ self.default_form_builder = LabelledFormBuilder
+
+ output_buffer = fields(:post, model: @post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_lazy_loading_form_builder_override
+ self.default_form_builder = "FormWithActsLikeFormForTest::LabelledFormBuilder"
+
+ output_buffer = fields(:post, model: @post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_with_labelled_builder
+ output_buffer = fields(:post, model: @post, builder: LabelledFormBuilder) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ 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/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_labelled_builder_with_nested_fields_without_options_hash
+ klass = nil
+
+ form_with(model: @post, builder: LabelledFormBuilder) do |f|
+ f.fields(:comments, model: Comment.new) do |nested_fields|
+ klass = nested_fields.class
+ ""
+ end
+ end
+
+ assert_equal LabelledFormBuilder, klass
+ end
+
+ def test_form_with_with_labelled_builder_with_nested_fields_with_options_hash
+ klass = nil
+
+ form_with(model: @post, builder: LabelledFormBuilder) do |f|
+ f.fields(:comments, model: Comment.new, index: "foo") do |nested_fields|
+ klass = nested_fields.class
+ ""
+ end
+ end
+
+ assert_equal LabelledFormBuilder, klass
+ end
+
+ def test_form_with_with_labelled_builder_path
+ path = nil
+
+ form_with(model: @post, builder: LabelledFormBuilder) do |f|
+ path = f.to_partial_path
+ ""
+ end
+
+ assert_equal "labelled_form", path
+ end
+
+ class LabelledFormBuilderSubclass < LabelledFormBuilder; end
+
+ def test_form_with_with_labelled_builder_with_nested_fields_with_custom_builder
+ klass = nil
+
+ form_with(model: @post, builder: LabelledFormBuilder) do |f|
+ f.fields(:comments, model: Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields|
+ klass = nested_fields.class
+ ""
+ end
+ end
+
+ assert_equal LabelledFormBuilderSubclass, klass
+ end
+
+ def test_form_with_with_html_options_adds_options_to_form_tag
+ form_with(model: @post, html: { id: "some_form", class: "some_class", multipart: true }) do |f| end
+ expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data")
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_string_url_option
+ form_with(model: @post, url: "http://www.otherdomain.com") do |f| end
+
+ assert_dom_equal whole_form("http://www.otherdomain.com", method: "patch"), output_buffer
+ end
+
+ def test_form_with_with_hash_url_option
+ form_with(model: @post, url: { controller: "controller", action: "action" }) do |f| end
+
+ assert_equal "controller", @url_for_options[:controller]
+ assert_equal "action", @url_for_options[:action]
+ end
+
+ def test_form_with_with_record_url_option
+ form_with(model: @post, url: @post) do |f| end
+
+ expected = whole_form("/posts/123", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_existing_object
+ form_with(model: @post) do |f| end
+
+ expected = whole_form("/posts/123", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_new_object
+ post = Post.new
+ post.persisted = false
+ def post.to_key; nil; end
+
+ form_with(model: post) {}
+
+ expected = whole_form("/posts")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_existing_object_in_list
+ @comment.save
+ form_with(model: [@post, @comment]) {}
+
+ expected = whole_form(post_comment_path(@post, @comment), method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_new_object_in_list
+ form_with(model: [@post, @comment]) {}
+
+ expected = whole_form(post_comments_path(@post))
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_existing_object_and_namespace_in_list
+ @comment.save
+ form_with(model: [:admin, @post, @comment]) {}
+
+ expected = whole_form(admin_post_comment_path(@post, @comment), method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_new_object_and_namespace_in_list
+ form_with(model: [:admin, @post, @comment]) {}
+
+ expected = whole_form(admin_post_comments_path(@post))
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_existing_object_and_custom_url
+ form_with(model: @post, url: "/super_posts") do |f| end
+
+ expected = whole_form("/super_posts", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_default_method_as_patch
+ form_with(model: @post) {}
+ expected = whole_form("/posts/123", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_with_data_attributes
+ form_with(model: @post, data: { behavior: "stuff" }) {}
+ assert_match %r|data-behavior="stuff"|, output_buffer
+ assert_match %r|data-remote="true"|, output_buffer
+ end
+
+ def test_fields_returns_block_result
+ output = fields(model: Post.new) { |f| "fields" }
+ assert_equal "fields", output
+ end
+
+ def test_form_with_only_instantiates_builder_once
+ initialization_count = 0
+ builder_class = Class.new(ActionView::Helpers::FormBuilder) do
+ define_method :initialize do |*args|
+ super(*args)
+ initialization_count += 1
+ end
+ end
+
+ form_with(model: @post, builder: builder_class) {}
+ assert_equal 1, initialization_count, "form builder instantiated more than once"
+ end
+
+ private
+ def hidden_fields(options = {})
+ method = options[:method]
+
+ if options.fetch(:skip_enforcing_utf8, false)
+ txt = ""
+ else
+ txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ end
+
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+
+ txt
+ end
+
+ def form_text(action = "/", id = nil, html_class = nil, local = nil, multipart = nil, method = nil)
+ txt = %{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if multipart
+ txt << %{ data-remote="true"} unless local
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ method = method.to_s == "get" ? "get" : "post"
+ txt << %{ method="#{method}">}
+ end
+
+ def whole_form(action = "/", id = nil, html_class = nil, local: false, **options)
+ contents = block_given? ? yield : ""
+
+ method, multipart = options.values_at(:method, :multipart)
+
+ form_text(action, id, html_class, local, multipart, method) + hidden_fields(options.slice :method, :skip_enforcing_utf8) + contents + "</form>"
+ end
+
+ def protect_against_forgery?
+ false
+ end
+
+ def with_locale(testing_locale = :label)
+ old_locale, I18n.locale = I18n.locale, testing_locale
+ yield
+ ensure
+ I18n.locale = old_locale
+ end
+end
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index 022bf315ce..b3a180b28a 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -257,11 +257,11 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_non_active_record_object
- form_for(OpenStruct.new(name: "ok"), as: "person", url: "an_url", html: { id: "create-person" }) do |f|
+ form_for(OpenStruct.new(name: "ok"), as: "person", url: "/an", html: { id: "create-person" }) do |f|
f.label(:name)
end
- expected = whole_form("an_url", "create-person", "new_person", method: "post") do
+ expected = whole_form("/an", "create-person", "new_person", method: "post") do
'<label for="person_name">Name</label>'
end
@@ -767,7 +767,6 @@ class FormHelperTest < ActionView::TestCase
'<input name="post[bar][comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_bar_comment_ids_3" name="post[bar][comment_ids][]" type="checkbox" value="3" />',
check_box("post", "comment_ids", { multiple: true, index: "bar" }, 3)
)
-
end
def test_checkbox_disabled_disables_hidden_field
@@ -1515,13 +1514,13 @@ class FormHelperTest < ActionView::TestCase
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>" +
+ "<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
@@ -1536,10 +1535,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "<label for='post_active_true'>true</label>" +
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \
+ "<label for='post_active_true'>true</label>" \
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \
"<label for='post_active_false'>false</label>"
end
@@ -1558,12 +1557,12 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<label for='post_active_true'>" +
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "true</label>" +
- "<label for='post_active_false'>" +
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<label for='post_active_true'>" \
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \
"false</label>"
end
@@ -1584,13 +1583,13 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post_1", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<label for='post_active_true'>" +
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "true</label>" +
- "<label for='post_active_false'>" +
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "false</label>" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<label for='post_active_true'>" \
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \
+ "false</label>" \
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1606,10 +1605,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "foo_new_post", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
- "<label for='foo_post_active_true'>true</label>" +
- "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" \
+ "<label for='foo_post_active_true'>true</label>" \
+ "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" \
"<label for='foo_post_active_false'>false</label>"
end
@@ -1625,10 +1624,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input type='hidden' name='post[1][active]' value='' />" +
- "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
- "<label for='post_1_active_true'>true</label>" +
- "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[1][active]' value='' />" \
+ "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" \
+ "<label for='post_1_active_true'>true</label>" \
+ "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" \
"<label for='post_1_active_false'>false</label>"
end
@@ -1644,12 +1643,12 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />" +
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "<label for='post_tag_ids_1'>Tag 1</label>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "<label for='post_tag_ids_2'>Tag 2</label>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "<label for='post_tag_ids_1'>Tag 1</label>" \
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "<label for='post_tag_ids_2'>Tag 2</label>" \
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \
"<label for='post_tag_ids_3'>Tag 3</label>"
end
@@ -1668,15 +1667,15 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />" +
- "<label for='post_tag_ids_1'>" +
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "Tag 1</label>" +
- "<label for='post_tag_ids_2'>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "Tag 2</label>" +
- "<label for='post_tag_ids_3'>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<label for='post_tag_ids_1'>" \
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \
"Tag 3</label>"
end
@@ -1698,16 +1697,16 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post_1", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />" +
- "<label for='post_tag_ids_1'>" +
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "Tag 1</label>" +
- "<label for='post_tag_ids_2'>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "Tag 2</label>" +
- "<label for='post_tag_ids_3'>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
- "Tag 3</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<label for='post_tag_ids_1'>" \
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \
+ "Tag 3</label>" \
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1724,8 +1723,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "foo_new_post", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />" +
- "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
"<label for='foo_post_tag_ids_1'>Tag 1</label>"
end
@@ -1742,8 +1741,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input name='post[1][tag_ids][]' type='hidden' value='' />" +
- "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" +
+ "<input name='post[1][tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" \
"<label for='post_1_tag_ids_1'>Tag 1</label>"
end
@@ -1751,8 +1750,6 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_file_field_generate_multipart
- Post.send :attr_accessor, :file
-
form_for(@post, html: { id: "create-post" }) do |f|
concat f.file_field(:file)
end
@@ -1765,8 +1762,6 @@ class FormHelperTest < ActionView::TestCase
end
def test_fields_for_with_file_field_generate_multipart
- Comment.send :attr_accessor, :file
-
form_for(@post) do |f|
concat f.fields_for(:comment, @post) { |c|
concat c.file_field(:file)
@@ -1801,7 +1796,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do
- "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" +
+ "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" \
"<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />"
end
@@ -1818,11 +1813,11 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "create-post", "edit_other_name", method: "patch") do
- "<label for='other_name_title' class='post_title'>Title</label>" +
- "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" +
- "<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' id='other_name_secret' value='1' type='checkbox' />" +
+ "<label for='other_name_title' class='post_title'>Title</label>" \
+ "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" \
+ "<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' id='other_name_secret' value='1' type='checkbox' />" \
"<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />"
end
@@ -1833,9 +1828,9 @@ class FormHelperTest < ActionView::TestCase
obj = Class.new do
private
- def private_property
- raise "This method should not be called."
- end
+ def private_property
+ raise "This method should not be called."
+ end
end.new
form_for(obj, as: "other_name", url: "/", html: { id: "edit-other-name" }) do |f|
@@ -1851,9 +1846,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/", "create-post", "edit_post", method: "delete") do
- "<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[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' />"
end
@@ -1868,9 +1863,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/", "create-post", "edit_post", method: "delete") do
- "<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[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' />"
end
@@ -1899,9 +1894,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do
- "<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[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' />"
end
@@ -1940,9 +1935,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do
- "<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[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' />"
end
@@ -1959,9 +1954,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post", remote: true) do
- "<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[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' />"
end
@@ -1977,9 +1972,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/", "create-post") do
- "<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[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' />"
end
@@ -1995,10 +1990,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do
- "<label for='post_123_title'>Title</label>" +
- "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
- "<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' />" +
+ "<label for='post_123_title'>Title</label>" \
+ "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \
+ "<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' id='post_123_secret' value='1' />"
end
@@ -2013,9 +2008,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do
- "<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[][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' />"
end
@@ -2030,8 +2025,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", 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' id='post_author_name' value='' /></div>" +
+ "<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' id='post_author_name' value='' /></div>" \
"<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
@@ -2048,8 +2043,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", 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' id='post_author_name' value='' /></div>" +
+ "<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' id='post_author_name' value='' /></div>" \
"<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
end
@@ -2063,7 +2058,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" +
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" \
"<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>"
end
@@ -2078,9 +2073,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do
- "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" \
+ "<textarea name='post[body]' id='namespace_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='namespace_post_secret' value='1' />"
end
@@ -2102,7 +2097,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do
- "<label for='namespace_post_title'>Title</label>" +
+ "<label for='namespace_post_title'>Title</label>" \
"<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />"
end
@@ -2128,7 +2123,7 @@ class FormHelperTest < ActionView::TestCase
end
expected_1 = whole_form("/posts/123", "namespace_1_edit_post_123", "edit_post", method: "patch") do
- "<label for='namespace_1_post_title'>Title</label>" +
+ "<label for='namespace_1_post_title'>Title</label>" \
"<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />"
end
@@ -2140,7 +2135,7 @@ class FormHelperTest < ActionView::TestCase
end
expected_2 = whole_form("/posts/123", "namespace_2_edit_post_123", "edit_post", method: "patch") do
- "<label for='namespace_2_post_title'>Title</label>" +
+ "<label for='namespace_2_post_title'>Title</label>" \
"<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />"
end
@@ -2158,8 +2153,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do
- "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" \
+ "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" \
"<input name='post[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />"
end
@@ -2266,11 +2261,13 @@ class FormHelperTest < ActionView::TestCase
concat f.fields_for("comment[]", @comment) { |c|
concat c.text_field(:name)
}
+ concat f.text_field(:body)
end
expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do
- "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
- "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />"
+ "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \
+ "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />" \
+ "<input name='post[123][body]' type='text' id='post_123_body' value='Back to the hill and over it again!' />"
end
assert_dom_equal expected, output_buffer
@@ -2285,7 +2282,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" +
+ "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" \
"<input name='post[1][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />"
end
@@ -2395,7 +2392,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
'<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="new author" />'
end
@@ -2422,8 +2419,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \
'<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
end
@@ -2441,8 +2438,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \
'<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
end
@@ -2460,7 +2457,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
'<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
end
@@ -2478,7 +2475,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
'<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
end
@@ -2496,8 +2493,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \
'<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
end
@@ -2516,8 +2513,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \
'<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
end
@@ -2537,10 +2534,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
'<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2564,10 +2561,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2591,9 +2588,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2617,10 +2614,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2640,10 +2637,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
'<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2664,10 +2661,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
end
@@ -2687,8 +2684,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
end
@@ -2708,9 +2705,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
end
@@ -2743,10 +2740,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
'<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2764,10 +2761,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
'<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2799,10 +2796,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \
'<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
end
@@ -2822,9 +2819,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
end
@@ -2842,7 +2839,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \
'<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
end
@@ -2859,7 +2856,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \
'<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
end
@@ -2882,7 +2879,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \
'<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
end
@@ -2968,17 +2965,17 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' +
- '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
- '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' +
- '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' +
- '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' +
- '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' +
- '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' +
- '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' +
- '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \
+ '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' \
+ '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' \
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \
+ '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' \
+ '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' \
+ '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' \
+ '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' \
+ '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' \
+ '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' \
+ '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' \
'<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />'
end
@@ -3009,9 +3006,9 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<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[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' />"
assert_dom_equal expected, output_buffer
@@ -3025,9 +3022,9 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
- "<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][title]' type='text' id='post_123_title' value='Hello World' />" \
+ "<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' id='post_123_secret' value='1' />"
assert_dom_equal expected, output_buffer
@@ -3041,9 +3038,9 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<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[][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' />"
assert_dom_equal expected, output_buffer
@@ -3057,9 +3054,9 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" +
- "<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][title]' type='text' id='post_abc_title' value='Hello World' />" \
+ "<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' id='post_abc_secret' value='1' />"
assert_dom_equal expected, output_buffer
@@ -3073,9 +3070,9 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<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[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' />"
assert_dom_equal expected, output_buffer
@@ -3089,9 +3086,9 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<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[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' />"
assert_dom_equal expected, output_buffer
@@ -3103,7 +3100,7 @@ class FormHelperTest < ActionView::TestCase
concat f.text_field(:title)
end
- assert_dom_equal "<label for=\"author_post_title\">Title</label>" +
+ assert_dom_equal "<label for=\"author_post_title\">Title</label>" \
"<input name='author[post][title]' type='text' id='author_post_title' value='Hello World' />",
output_buffer
end
@@ -3114,7 +3111,7 @@ class FormHelperTest < ActionView::TestCase
concat f.text_field(:title)
end
- assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" +
+ assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \
"<input name='author[post][1][title]' type='text' id='author_post_1_title' value='Hello World' />",
output_buffer
end
@@ -3134,9 +3131,9 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
- "<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='parent_post[secret]' type='hidden' value='0' />" +
+ "<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='parent_post[secret]' type='hidden' value='0' />" \
"<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />"
end
@@ -3154,8 +3151,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
- "<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[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[comment][name]' type='text' id='post_comment_name' value='new comment' />"
end
@@ -3194,8 +3191,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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' id='post_secret' value='1' /><br/>"
end
@@ -3213,8 +3210,8 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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' id='post_secret' value='1' /><br/>"
end
@@ -3272,8 +3269,8 @@ class FormHelperTest < ActionView::TestCase
end
expected =
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><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' id='post_secret' value='1' /><br/>"
assert_dom_equal expected, output_buffer
@@ -3443,7 +3440,7 @@ class FormHelperTest < ActionView::TestCase
assert_equal 1, initialization_count, "form builder instantiated more than once"
end
- protected
+ private
def hidden_fields(options = {})
method = options[:method]
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index 51f5d25958..258dcdb806 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -393,30 +393,30 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_options_no_params
opts = time_zone_options_for_select
- assert_dom_equal "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>\n" +
+ assert_dom_equal "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\">D</option>\n" \
"<option value=\"E\">E</option>",
opts
end
def test_time_zone_options_with_selected
opts = time_zone_options_for_select("D")
- assert_dom_equal "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ assert_dom_equal "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
"<option value=\"E\">E</option>",
opts
end
def test_time_zone_options_with_unknown_selected
opts = time_zone_options_for_select("K")
- assert_dom_equal "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>\n" +
+ assert_dom_equal "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\">D</option>\n" \
"<option value=\"E\">E</option>",
opts
end
@@ -424,11 +424,11 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_options_with_priority_zones
zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ]
opts = time_zone_options_for_select(nil, zones)
- assert_dom_equal "<option value=\"B\">B</option>\n" +
- "<option value=\"E\">E</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"C\">C</option>\n" +
+ assert_dom_equal "<option value=\"B\">B</option>\n" \
+ "<option value=\"E\">E</option>" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"C\">C</option>\n" \
"<option value=\"D\">D</option>",
opts
end
@@ -436,11 +436,11 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_options_with_selected_priority_zones
zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ]
opts = time_zone_options_for_select("E", zones)
- assert_dom_equal "<option value=\"B\">B</option>\n" +
- "<option value=\"E\" selected=\"selected\">E</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"C\">C</option>\n" +
+ assert_dom_equal "<option value=\"B\">B</option>\n" \
+ "<option value=\"E\" selected=\"selected\">E</option>" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"C\">C</option>\n" \
"<option value=\"D\">D</option>",
opts
end
@@ -448,11 +448,11 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_options_with_unselected_priority_zones
zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ]
opts = time_zone_options_for_select("C", zones)
- assert_dom_equal "<option value=\"B\">B</option>\n" +
- "<option value=\"E\">E</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"C\" selected=\"selected\">C</option>\n" +
+ assert_dom_equal "<option value=\"B\">B</option>\n" \
+ "<option value=\"E\">E</option>" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"C\" selected=\"selected\">C</option>\n" \
"<option value=\"D\">D</option>",
opts
end
@@ -1031,12 +1031,12 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_select
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
@@ -1049,12 +1049,12 @@ class FormOptionsHelperTest < ActionView::TestCase
end
assert_dom_equal(
- "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
output_buffer
)
@@ -1068,12 +1068,12 @@ class FormOptionsHelperTest < ActionView::TestCase
end
assert_dom_equal(
- "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
output_buffer
)
@@ -1088,12 +1088,12 @@ class FormOptionsHelperTest < ActionView::TestCase
end
assert_dom_equal(
- "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
output_buffer
)
@@ -1102,13 +1102,13 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_select_with_blank
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone", nil, include_blank: true)
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"\"></option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"\"></option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
@@ -1116,13 +1116,13 @@ class FormOptionsHelperTest < ActionView::TestCase
def test_time_zone_select_with_blank_as_string
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone", nil, include_blank: "No Zone")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"\">No Zone</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"\">No Zone</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
@@ -1131,12 +1131,12 @@ class FormOptionsHelperTest < ActionView::TestCase
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone", nil, {},
"style" => "color: red")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {},
@@ -1147,13 +1147,13 @@ class FormOptionsHelperTest < ActionView::TestCase
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone", nil,
{ include_blank: true }, "style" => "color: red")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
- "<option value=\"\"></option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \
+ "<option value=\"\"></option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
@@ -1164,13 +1164,13 @@ class FormOptionsHelperTest < ActionView::TestCase
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone", nil,
{ include_blank: "No Zone" }, "style" => "color: red")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
- "<option value=\"\">No Zone</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \
+ "<option value=\"\">No Zone</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
@@ -1181,13 +1181,13 @@ class FormOptionsHelperTest < ActionView::TestCase
@firm = Firm.new("D")
zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
html = time_zone_select("firm", "time_zone", zones)
- assert_dom_equal "<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>" +
+ assert_dom_equal "<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>",
html
end
@@ -1200,13 +1200,13 @@ class FormOptionsHelperTest < ActionView::TestCase
end
html = time_zone_select("firm", "time_zone", /A|D/)
- assert_dom_equal "<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>" +
+ assert_dom_equal "<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>",
html
end
@@ -1221,13 +1221,13 @@ class FormOptionsHelperTest < ActionView::TestCase
end
html = time_zone_select("firm", "time_zone", /A|D/)
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
@@ -1237,12 +1237,12 @@ class FormOptionsHelperTest < ActionView::TestCase
@firm.time_zone = nil
html = time_zone_select("firm", "time_zone", nil, default: "B")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\" selected=\"selected\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\" selected=\"selected\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
@@ -1251,12 +1251,12 @@ class FormOptionsHelperTest < ActionView::TestCase
@firm = Firm.new("D")
html = time_zone_select("firm", "time_zone", nil, default: "B")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 24ae6b8b90..084c540139 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -510,6 +510,13 @@ class FormTagHelperTest < ActionView::TestCase
)
end
+ def test_submit_tag_doesnt_have_data_disable_with_twice_with_hash
+ assert_equal(
+ %(<input type="submit" name="commit" value="Save" data-disable-with="Processing..." />),
+ submit_tag("Save", data: { disable_with: "Processing..." })
+ )
+ end
+
def test_submit_tag_with_symbol_value
assert_dom_equal(
%(<input data-disable-with="Save" name='commit' type="submit" value="Save" />),
@@ -694,31 +701,31 @@ class FormTagHelperTest < ActionView::TestCase
def test_text_area_tag_options_symbolize_keys_side_effects
options = { option: "random_option" }
text_area_tag "body", "hello world", options
- assert_equal options, option: "random_option"
+ assert_equal({ option: "random_option" }, options)
end
def test_submit_tag_options_symbolize_keys_side_effects
options = { option: "random_option" }
submit_tag "submit value", options
- assert_equal options, option: "random_option"
+ assert_equal({ option: "random_option" }, options)
end
def test_button_tag_options_symbolize_keys_side_effects
options = { option: "random_option" }
button_tag "button value", options
- assert_equal options, option: "random_option"
+ assert_equal({ option: "random_option" }, options)
end
def test_image_submit_tag_options_symbolize_keys_side_effects
options = { option: "random_option" }
image_submit_tag "submit source", options
- assert_equal options, option: "random_option"
+ assert_equal({ option: "random_option" }, options)
end
def test_image_label_tag_options_symbolize_keys_side_effects
options = { option: "random_option" }
label_tag "submit source", "title", options
- assert_equal options, option: "random_option"
+ assert_equal({ option: "random_option" }, options)
end
def protect_against_forgery?
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
index 2a2ada2b36..678120a9c9 100644
--- a/actionview/test/template/number_helper_test.rb
+++ b/actionview/test/template/number_helper_test.rb
@@ -4,7 +4,7 @@ class NumberHelperTest < ActionView::TestCase
tests ActionView::Helpers::NumberHelper
def test_number_to_phone
- assert_equal nil, number_to_phone(nil)
+ assert_nil number_to_phone(nil)
assert_equal "555-1234", number_to_phone(5551234)
assert_equal "(800) 555-1212 x 123", number_to_phone(8005551212, area_code: true, extension: 123)
assert_equal "+18005551212", number_to_phone(8005551212, country_code: 1, delimiter: "")
@@ -13,7 +13,7 @@ class NumberHelperTest < ActionView::TestCase
end
def test_number_to_currency
- assert_equal nil, number_to_currency(nil)
+ assert_nil number_to_currency(nil)
assert_equal "$1,234,567,890.50", number_to_currency(1234567890.50)
assert_equal "$1,234,567,892", number_to_currency(1234567891.50, precision: 0)
assert_equal "1,234,567,890.50 - K&#269;", number_to_currency("-1234567890.50", unit: raw("K&#269;"), format: "%n %u", negative_format: "%n - %u")
@@ -25,7 +25,7 @@ class NumberHelperTest < ActionView::TestCase
end
def test_number_to_percentage
- assert_equal nil, number_to_percentage(nil)
+ assert_nil number_to_percentage(nil)
assert_equal "100.000%", number_to_percentage(100)
assert_equal "100.000 %", number_to_percentage(100, format: "%n %")
assert_equal "&lt;b&gt;100.000&lt;/b&gt; %", number_to_percentage(100, format: "<b>%n</b> %")
@@ -43,13 +43,13 @@ class NumberHelperTest < ActionView::TestCase
end
def test_number_with_delimiter
- assert_equal nil, number_with_delimiter(nil)
+ assert_nil number_with_delimiter(nil)
assert_equal "12,345,678", number_with_delimiter(12345678)
assert_equal "0", number_with_delimiter(0)
end
def test_number_with_precision
- assert_equal nil, number_with_precision(nil)
+ assert_nil number_with_precision(nil)
assert_equal "-111.235", number_with_precision(-111.2346)
assert_equal "111.00", number_with_precision(111, precision: 2)
assert_equal "0.00100", number_with_precision(0.001, precision: 5)
@@ -57,13 +57,13 @@ class NumberHelperTest < ActionView::TestCase
end
def test_number_to_human_size
- assert_equal nil, number_to_human_size(nil)
+ assert_nil number_to_human_size(nil)
assert_equal "3 Bytes", number_to_human_size(3.14159265)
assert_equal "1.2 MB", number_to_human_size(1234567, precision: 2)
end
def test_number_to_human
- assert_equal nil, number_to_human(nil)
+ assert_nil number_to_human(nil)
assert_equal "0", number_to_human(0)
assert_equal "1.23 Thousand", number_to_human(1234)
assert_equal "489.0 Thousand", number_to_human(489000, precision: 4, strip_insignificant_zeros: false)
diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
index 8691bb11ee..537b4393ee 100644
--- a/actionview/test/template/output_safety_helper_test.rb
+++ b/actionview/test/template/output_safety_helper_test.rb
@@ -33,6 +33,22 @@ class OutputSafetyHelperTest < ActionView::TestCase
assert_equal "&quot;a&quot; &lt;br/&gt; &lt;b&gt; &lt;br/&gt; &lt;c&gt;", joined
end
+ test "safe_join should return the safe string separated by $, when second argument is not passed" do
+ default_delimeter = $,
+
+ begin
+ $, = nil
+ joined = safe_join(["a", "b"])
+ assert_equal "ab", joined
+
+ $, = "|"
+ joined = safe_join(["a", "b"])
+ assert_equal "a|b", joined
+ ensure
+ $, = default_delimeter
+ end
+ end
+
test "to_sentence should escape non-html_safe values" do
actual = to_sentence(%w(< > & ' "))
assert actual.html_safe?
@@ -87,4 +103,15 @@ class OutputSafetyHelperTest < ActionView::TestCase
assert_equal "one, two three", to_sentence(["one", "two", "three"], last_word_connector: " ")
assert_equal "one, two and three", to_sentence(["one", "two", "three"], last_word_connector: " and ")
end
+
+ test "to_sentence is not affected by $," do
+ separator_was = $,
+ $, = "|"
+ begin
+ assert_equal "one and two", to_sentence(["one", "two"])
+ assert_equal "one, two, and three", to_sentence(["one", "two", "three"])
+ ensure
+ $, = separator_was
+ end
+ end
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index e7e0b147c7..412948719c 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -220,15 +220,15 @@ module RenderTestCases
def test_render_partial_with_invalid_option_as
e = assert_raises(ArgumentError) { @view.render(partial: "test/partial_only", as: "a-in") }
- assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " +
- "make sure it starts with lowercase letter, " +
+ assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \
+ "make sure it starts with lowercase letter, " \
"and is followed by any combination of letters, numbers and underscores.", e.message
end
def test_render_partial_with_hyphen_and_invalid_option_as
e = assert_raises(ArgumentError) { @view.render(partial: "test/a-in", as: "a-in") }
- assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " +
- "make sure it starts with lowercase letter, " +
+ assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \
+ "make sure it starts with lowercase letter, " \
"and is followed by any combination of letters, numbers and underscores.", e.message
end
@@ -253,7 +253,7 @@ module RenderTestCases
def test_render_sub_template_with_errors
e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/sub_template_raise") }
assert_match %r!method.*doesnt_exist!, e.message
- assert_equal "Trace of template inclusion: #{File.expand_path("#{FIXTURE_LOAD_PATH}/test/sub_template_raise.html.erb")}", e.sub_template_message
+ assert_match %r{Trace of template inclusion: .*test/sub_template_raise.html.erb}, e.sub_template_message
assert_equal "1", e.line_number
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
@@ -301,6 +301,15 @@ module RenderTestCases
@view.render(partial: "test/local_inspector", collection: [ Customer.new("mary") ])
end
+ def test_render_partial_collection_with_different_partials_still_provides_partial_iteration
+ a = {}
+ b = {}
+ def a.to_partial_path; "test/partial_iteration_1"; end
+ def b.to_partial_path; "test/partial_iteration_2"; end
+
+ assert_equal "local-variable\nlocal-variable", @controller_view.render([a, b])
+ end
+
def test_render_partial_with_empty_collection_should_return_nil
assert_nil @view.render(partial: "test/customer", collection: [])
end
@@ -393,13 +402,11 @@ module RenderTestCases
assert_equal :partial_name_local_variable, exception.cause.name
end
- # TODO: The reason for this test is unclear, improve documentation
- def test_render_partial_and_fallback_to_layout
+ def test_render_partial_with_no_block_given_to_yield
assert_equal "Before (Josh)\n\nAfter", @view.render(partial: "test/layout_for_partial", locals: { name: "Josh" })
end
- # TODO: The reason for this test is unclear, improve documentation
- def test_render_missing_xml_partial_and_raise_missing_template
+ def test_render_partial_with_non_existent_format_and_raise_missing_template
@view.formats = [:xml]
assert_raises(ActionView::MissingTemplate) { @view.render(partial: "test/layout_for_partial") }
ensure
@@ -424,7 +431,7 @@ module RenderTestCases
end
CustomHandler = lambda do |template|
- "@output_buffer = ''\n" +
+ "@output_buffer = ''\n" \
"@output_buffer << 'source: #{template.source.inspect}'\n"
end
@@ -475,11 +482,9 @@ module RenderTestCases
end
def test_render_ignores_templates_with_malformed_template_handlers
- ActiveSupport::Deprecation.silence do
- %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name|
- assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists"
- assert_raises(ActionView::MissingTemplate) { @view.render(file: "test/malformed/#{name}") }
- end
+ %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name|
+ assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists"
+ assert_raises(ActionView::MissingTemplate) { @view.render(file: "test/malformed/#{name}") }
end
end
diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index c8963fee9c..11ed55456f 100644
--- a/actionview/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -10,6 +10,7 @@ class SanitizeHelperTest < ActionView::TestCase
assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
+ assert_equal "&lt;malformed &amp; link", strip_links('<<a href="https://example.org">malformed & link</a>')
end
def test_sanitize_form
@@ -26,6 +27,7 @@ class SanitizeHelperTest < ActionView::TestCase
assert_equal("Dont touch me", strip_tags("Dont touch me"))
assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.")
+ assert_equal("Jekyll &amp; Hyde", strip_tags("Jekyll & Hyde"))
assert_equal "", strip_tags("<script>")
end
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 41225000f0..3deddd5706 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -50,7 +50,7 @@ module ActionView
end
test "retrieve non existing config values" do
- assert_equal nil, ActionView::Base.new.config.something_odd
+ assert_nil ActionView::Base.new.config.something_odd
end
test "works without testing a helper module" do
diff --git a/actionview/test/template/text_test.rb b/actionview/test/template/text_test.rb
index 6510688f97..72ffd5f3be 100644
--- a/actionview/test/template/text_test.rb
+++ b/actionview/test/template/text_test.rb
@@ -1,17 +1,23 @@
require "abstract_unit"
class TextTest < ActiveSupport::TestCase
- test "formats returns symbol for recognized MIME type" do
- assert_equal [:text], ActionView::Template::Text.new("", :text).formats
+ test "formats always return :text" do
+ assert_equal [:text], ActionView::Template::Text.new("").formats
end
- test "formats returns string for recognized MIME type when MIME does not have symbol" do
- foo = Mime::Type.lookup("foo")
- assert_nil foo.to_sym
- assert_equal ["foo"], ActionView::Template::Text.new("", foo).formats
+ test "identifier should return 'text template'" do
+ assert_equal "text template", ActionView::Template::Text.new("").identifier
end
- test "formats returns string for unknown MIME type" do
- assert_equal ["foo"], ActionView::Template::Text.new("", "foo").formats
+ test "inspect should return 'text template'" do
+ assert_equal "text template", ActionView::Template::Text.new("").inspect
+ end
+
+ test "to_str should return a given string" do
+ assert_equal "a cat", ActionView::Template::Text.new("a cat").to_str
+ end
+
+ test "render should return a given string" do
+ assert_equal "a dog", ActionView::Template::Text.new("a dog").render
end
end
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 5a2319fe96..09454b32cc 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -235,14 +235,14 @@ class UrlHelperTest < ActiveSupport::TestCase
end
end
- def test_button_to_with_permited_strong_params
+ def test_button_to_with_permitted_strong_params
assert_dom_equal(
%{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>},
button_to("Hello", "http://www.example.com", params: FakeParams.new)
)
end
- def test_button_to_with_unpermited_strong_params
+ def test_button_to_with_unpermitted_strong_params
assert_raises(ArgumentError) do
button_to("Hello", "http://www.example.com", params: FakeParams.new(false))
end
@@ -496,6 +496,15 @@ class UrlHelperTest < ActiveSupport::TestCase
assert current_page?("http://www.example.com/")
end
+ 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)
+ end
+
def test_current_page_with_params_that_match
@request = request_for_url("/?order=desc&page=1")
@@ -810,9 +819,9 @@ class TasksController < ActionController::Base
render_default
end
- protected
+ private
def render_default
- render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" +
+ render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" \
"<%= link_to_unless_current('tasks', tasks_url) %>"
end
end
diff --git a/actionview/test/ujs/.gitignore b/actionview/test/ujs/.gitignore
new file mode 100644
index 0000000000..31dbbff57c
--- /dev/null
+++ b/actionview/test/ujs/.gitignore
@@ -0,0 +1 @@
+/log
diff --git a/actionview/test/ujs/config.ru b/actionview/test/ujs/config.ru
new file mode 100644
index 0000000000..48b7a4b53a
--- /dev/null
+++ b/actionview/test/ujs/config.ru
@@ -0,0 +1,4 @@
+$LOAD_PATH.unshift File.expand_path("..", __FILE__)
+require "server"
+
+run UJS::Server
diff --git a/actionview/test/ujs/public/test/.eslintrc.yml b/actionview/test/ujs/public/test/.eslintrc.yml
new file mode 100644
index 0000000000..06d7dd36ea
--- /dev/null
+++ b/actionview/test/ujs/public/test/.eslintrc.yml
@@ -0,0 +1,21 @@
+env:
+ browser: true
+extends: eslint:recommended
+rules:
+ no-undef: off
+ no-unused-vars: off
+ indent: off
+ linebreak-style: ['error', 'unix']
+ quotes: ['error', 'single']
+ semi: ['error', 'never']
+ no-shadow: ['error'] # Prevent potential errors
+ no-console: 'off'
+ # styles
+ space-before-function-paren: ['error', 'never']
+ space-before-blocks: 'error'
+ brace-style: ['error', '1tbs', { allowSingleLine: true }]
+ key-spacing: 'error'
+ array-bracket-spacing: 'error'
+ comma-spacing: 'error'
+ comma-dangle: 'off'
+ eol-last: 'error'
diff --git a/actionview/test/ujs/public/test/call-remote-callbacks.js b/actionview/test/ujs/public/test/call-remote-callbacks.js
new file mode 100644
index 0000000000..707e21541d
--- /dev/null
+++ b/actionview/test/ujs/public/test/call-remote-callbacks.js
@@ -0,0 +1,273 @@
+(function() {
+
+module('call-remote-callbacks', {
+ setup: function() {
+ $('#qunit-fixture').append($('<form />', {
+ action: '/echo', method: 'get', 'data-remote': 'true'
+ }))
+ },
+ teardown: function() {
+ $(document).undelegate('form[data-remote]', 'ajax:beforeSend')
+ $(document).undelegate('form[data-remote]', 'ajax:before')
+ $(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')
+}
+
+asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function() {
+ $('form[data-remote]')
+ .append($('<input type="text" name="user_name" value="john">'))
+ .append($('<input type="text" name="removed_user_name" value="john">'))
+ .bindNative('ajax:before', function() {
+ var form = $(this)
+ form
+ .append($('<input />', {name: 'other_user_name', value: 'jonathan'}))
+ .find('input[name="removed_user_name"]').remove()
+ form
+ .find('input[name="user_name"]').val('steve')
+ })
+
+ submit(function(form) {
+ form.bindNative('ajax:success', function(e, data, status, xhr) {
+ equal(data.params.user_name, 'steve', 'modified field value should have been submitted')
+ equal(data.params.other_user_name, 'jonathan', 'added field value should have been submitted')
+ equal(data.params.removed_user_name, undefined, 'removed field value should be undefined')
+ })
+ })
+})
+
+asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 2, function() {
+ $('form[data-remote]').data('type', 'html')
+ .bindNative('ajax:before', function() {
+ this.setAttribute('data-type', 'xml')
+ })
+
+ submit(function(form) {
+ form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
+ equal(settings.dataType, 'xml', 'modified dataType should have been requested')
+ })
+ })
+})
+
+asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function() {
+ $('form[data-remote]').data('with-credentials', false)
+ .bindNative('ajax:before', function() {
+ this.setAttribute('data-with-credentials', true)
+ })
+
+ submit(function(form) {
+ form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
+ equal(settings.withCredentials, true, 'setting modified in ajax:before should have forced withCredentials request')
+ })
+ })
+})
+
+asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() {
+ submit(function(form) {
+ form.bindNative('ajax:beforeSend', function() {
+ ok(true, 'aborting request in ajax:beforeSend')
+ return false
+ })
+ 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) {
+ ok(false, 'ajax:error should not run')
+ })
+ $(document).bindNative('ajaxStop', function() {
+ start()
+ })
+ })
+})
+
+function skipIt() {
+ // This test cannot work due to the security feature in browsers which makes the value
+ // attribute of file input fields readonly, so it cannot be set with default value.
+ // This is what the test would look like though if browsers let us automate this test.
+ asyncTest('non-blank file form input field should abort remote request, but submit normally', 5, function() {
+ var form = $('form[data-remote]')
+ .append($('<input type="file" name="attachment" value="default.png">'))
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax:beforeSend should not run')
+ })
+ .bind('iframe:loading', function() {
+ ok(true, 'form should get submitted')
+ })
+ .bindNative('ajax:aborted:file', function(e, data) {
+ ok(data.length == 1, 'ajax:aborted:file event is passed all non-blank file inputs (jQuery objects)')
+ ok(data.first().is('input[name="attachment"]'), 'ajax:aborted:file adds non-blank file input to data')
+ ok(true, 'ajax:aborted:file event should run')
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() {
+ form.find('input[type="file"]').val('')
+ form.unbind('ajax:beforeSend')
+ submit()
+ }, 13)
+ })
+
+ asyncTest('file form input field should not abort remote request if file form input does not have a name attribute', 5, function() {
+ var form = $('form[data-remote]')
+ .append($('<input type="file" value="default.png">'))
+ .bindNative('ajax:beforeSend', function() {
+ ok(true, 'ajax:beforeSend should run')
+ })
+ .bind('iframe:loading', function() {
+ ok(true, 'form should get submitted')
+ })
+ .bindNative('ajax:aborted:file', function(e, data) {
+ ok(false, 'ajax:aborted:file should not run')
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() {
+ form.find('input[type="file"]').val('')
+ form.unbind('ajax:beforeSend')
+ submit()
+ }, 13)
+ })
+
+ asyncTest('blank file input field should abort request entirely if handler bound to "ajax:aborted:file" event that returns false', 1, function() {
+ var form = $('form[data-remote]')
+ .append($('<input type="file" name="attachment" value="default.png">'))
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax:beforeSend should not run')
+ })
+ .bind('iframe:loading', function() {
+ ok(false, 'form should not get submitted')
+ })
+ .bindNative('ajax:aborted:file', function() {
+ return false
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() {
+ form.find('input[type="file"]').val('')
+ form.unbind('ajax:beforeSend')
+ submit()
+ }, 13)
+ })
+}
+
+asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() {
+ $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() {
+ ok(true, 'ajax:beforeSend observed with event delegation')
+ return false
+ })
+
+ 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() {
+ 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() {
+ submit(function(form) {
+ form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
+ ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object')
+ equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object')
+ })
+ form.bindNative('ajax:send', function(e, xhr) {
+ ok(xhr.abort, 'first argument to "ajax:send" should be an XHR object')
+ })
+ form.bindNative('ajax:success', function(e, data, status, xhr) {
+ ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object')
+ equal(status, 'OK', 'second argument to ajax:success should be a status string')
+ ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object')
+ })
+ form.bindNative('ajax:complete', function(e, xhr, status) {
+ ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object')
+ equal(status, 'OK', 'second argument to ajax:complete should be a status string')
+ })
+ })
+})
+
+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')
+ })
+ })
+ })
+}
+
+// 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() {
+ ok(true, 'ajax:beforeSend handler is triggered')
+ })
+ .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()
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() { start() }, 35)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js
new file mode 100644
index 0000000000..dbeb8ad832
--- /dev/null
+++ b/actionview/test/ujs/public/test/call-remote.js
@@ -0,0 +1,247 @@
+(function() {
+
+function buildForm(attrs) {
+ attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs)
+
+ $('#qunit-fixture').append($('<form />', attrs))
+ .find('form').append($('<input type="text" name="user_name" value="john">'))
+}
+
+module('call-remote')
+
+function submit(fn) {
+ $('form')
+ .bindNative('ajax:success', fn)
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('submit')
+}
+
+asyncTest('form method is read from "method" and not from "data-method"', 1, function() {
+ buildForm({ method: 'post', 'data-method': 'get' })
+
+ submit(function(e, data, status, xhr) {
+ App.assertPostRequest(data)
+ })
+})
+
+asyncTest('form method is not read from "data-method" attribute in case of missing "method"', 1, function() {
+ buildForm({ 'data-method': 'put' })
+
+ submit(function(e, data, status, xhr) {
+ App.assertGetRequest(data)
+ })
+})
+
+asyncTest('form method is read from submit button "formmethod" if submit is triggered by that button', 1, function() {
+ var submitButton = $('<input type="submit" formmethod="get">')
+ buildForm({ method: 'post' })
+
+ $('#qunit-fixture').find('form').append(submitButton)
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertGetRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+
+ submitButton.triggerNative('click')
+})
+
+asyncTest('form default method is GET', 1, function() {
+ buildForm()
+
+ submit(function(e, data, status, xhr) {
+ App.assertGetRequest(data)
+ })
+})
+
+asyncTest('form url is picked up from "action"', 1, function() {
+ buildForm({ method: 'post' })
+
+ submit(function(e, data, status, xhr) {
+ App.assertRequestPath(data, '/echo')
+ })
+})
+
+asyncTest('form url is read from "action" not "href"', 1, function() {
+ buildForm({ method: 'post', href: '/echo2' })
+
+ submit(function(e, data, status, xhr) {
+ App.assertRequestPath(data, '/echo')
+ })
+})
+
+asyncTest('form url is read from submit button "formaction" if submit is triggered by that button', 1, function() {
+ var submitButton = $('<input type="submit" formaction="/echo">')
+ buildForm({ method: 'post', href: '/echo2' })
+
+ $('#qunit-fixture').find('form').append(submitButton)
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertRequestPath(data, '/echo')
+ })
+ .bindNative('ajax:complete', function() { start() })
+
+ submitButton.triggerNative('click')
+})
+
+asyncTest('prefer JS, but accept any format', 1, function() {
+ buildForm({ method: 'post' })
+
+ submit(function(e, data, status, xhr) {
+ var accept = data.HTTP_ACCEPT
+ ok(accept.match(/text\/javascript.+\*\/\*/), 'Accept: ' + accept)
+ })
+})
+
+asyncTest('JS code should be executed', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'script' })
+
+ $('form').append('<input type="text" name="content_type" value="text/javascript">')
+ $('form').append('<input type="text" name="content" value="ok(true, \'remote code should be run\')">')
+
+ submit()
+})
+
+asyncTest('XML document should be parsed', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'html' })
+
+ $('form').append('<input type="text" name="content_type" value="application/xml">')
+ $('form').append('<input type="text" name="content" value="<p>hello</p>">')
+
+ submit(function(e, data, status, xhr) {
+ ok(data instanceof Document, 'returned data should be an XML document')
+ })
+})
+
+asyncTest('accept application/json if "data-type" is json', 1, function() {
+ buildForm({ method: 'post', 'data-type': 'json' })
+
+ submit(function(e, data, status, xhr) {
+ equal(data.HTTP_ACCEPT, 'application/json, text/javascript, */*; q=0.01')
+ })
+})
+
+asyncTest('allow empty "data-remote" attribute', 1, function() {
+ var form = $('#qunit-fixture').append($('<form action="/echo" data-remote />')).find('form')
+
+ submit(function() {
+ ok(true, 'form with empty "data-remote" attribute is also allowed')
+ })
+})
+
+asyncTest('query string in form action should be stripped in a GET request in normal submit', 1, function() {
+ buildForm({ action: '/echo?param1=abc', 'data-remote': 'false' })
+
+ $(document).one('iframe:loaded', function(e, data) {
+ equal(data.params.param1, undefined, '"param1" should not be passed to server')
+ start()
+ })
+
+ $('#qunit-fixture form').triggerNative('submit')
+})
+
+asyncTest('query string in form action should be stripped in a GET request in ajax submit', 1, function() {
+ buildForm({ action: '/echo?param1=abc' })
+
+ submit(function(e, data, status, xhr) {
+ equal(data.params.param1, undefined, '"param1" should not be passed to server')
+ })
+})
+
+asyncTest('query string in form action should not be stripped in a POST request in normal submit', 1, function() {
+ buildForm({ action: '/echo?param1=abc', method: 'post', 'data-remote': 'false' })
+
+ $(document).one('iframe:loaded', function(e, data) {
+ equal(data.params.param1, 'abc', '"param1" should be passed to server')
+ start()
+ })
+
+ $('#qunit-fixture form').triggerNative('submit')
+})
+
+asyncTest('query string in form action should not be stripped in a POST request in ajax submit', 1, function() {
+ buildForm({ action: '/echo?param1=abc', method: 'post' })
+
+ submit(function(e, data, status, xhr) {
+ equal(data.params.param1, 'abc', '"param1" should be passed to server')
+ })
+})
+
+asyncTest('allow empty form "action"', 1, function() {
+ var currentLocation, ajaxLocation
+
+ buildForm({ action: '' })
+
+ $('#qunit-fixture').find('form')
+ .bindNative('ajax:beforeSend', function(e, xhr, settings) {
+ // Get current location (the same way jQuery does)
+ try {
+ currentLocation = location.href
+ } catch(err) {
+ currentLocation = document.createElement( 'a' )
+ currentLocation.href = ''
+ currentLocation = currentLocation.href
+ }
+ currentLocation = currentLocation.replace(/\?.*$/, '')
+
+ // Actual location (strip out settings.data that jQuery serializes and appends)
+ // HACK: can no longer use settings.data below to see what was appended to URL, as of
+ // jQuery 1.6.3 (see http://bugs.jquery.com/ticket/10202 and https://github.com/jquery/jquery/pull/544)
+ ajaxLocation = settings.url.replace('user_name=john', '').replace(/&$/, '').replace(/\?$/, '')
+ equal(ajaxLocation.match(/^(.*)/)[1], currentLocation, 'URL should be current page by default')
+
+ // Prevent the request from actually getting sent to the current page and
+ // causing an error.
+ return false
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() { start() }, 13)
+})
+
+asyncTest('sends CSRF token in custom header', 1, function() {
+ buildForm({ method: 'post' })
+ $('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />')
+
+ submit(function(e, data, status, xhr) {
+ equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent')
+ })
+})
+
+asyncTest('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', 1, function() {
+
+ // Don't set data-cross-domain here, just set action to be a different domain than localhost
+ buildForm({ action: 'http://www.alfajango.com' })
+ $('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />')
+
+ $('#qunit-fixture').find('form')
+ .bindNative('ajax:beforeSend', function(evt, req, settings) {
+
+ equal(settings.crossDomain, true, 'crossDomain should be set to true')
+
+ // prevent request from actually getting sent off-domain
+ return false
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() { start() }, 13)
+})
+
+asyncTest('intelligently guesses crossDomain behavior when target URL consists of only a path', 1, function() {
+
+ // Don't set data-cross-domain here, just set action to be a different domain than localhost
+ buildForm({ action: '/just/a/path' })
+ $('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />')
+
+ $('#qunit-fixture').find('form')
+ .bindNative('ajax:beforeSend', function(evt, req, settings) {
+
+ equal(settings.crossDomain, false, 'crossDomain should be set to false')
+
+ // prevent request from actually getting sent off-domain
+ return false
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() { start() }, 13)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/csrf-refresh.js b/actionview/test/ujs/public/test/csrf-refresh.js
new file mode 100644
index 0000000000..e302042542
--- /dev/null
+++ b/actionview/test/ujs/public/test/csrf-refresh.js
@@ -0,0 +1,24 @@
+(function() {
+
+module('csrf-refresh', {})
+
+asyncTest('refresh all csrf tokens', 1, function() {
+ var correctToken = 'cf50faa3fe97702ca1ae'
+
+ var form = $('<form />')
+ var input = $('<input>').attr({ type: 'hidden', name: 'authenticity_token', id: 'authenticity_token', value: 'foo' })
+ input.appendTo(form)
+
+ $('#qunit-fixture')
+ .append('<meta name="csrf-param" content="authenticity_token"/>')
+ .append('<meta name="csrf-token" content="' + correctToken + '"/>')
+ .append(form)
+
+ $.rails.refreshCSRFTokens()
+ currentToken = $('#qunit-fixture #authenticity_token').val()
+
+ start()
+ equal(currentToken, correctToken)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/csrf-token.js b/actionview/test/ujs/public/test/csrf-token.js
new file mode 100644
index 0000000000..388b40e057
--- /dev/null
+++ b/actionview/test/ujs/public/test/csrf-token.js
@@ -0,0 +1,27 @@
+(function() {
+
+module('csrf-token', {})
+
+asyncTest('find csrf token', 1, function() {
+ var correctToken = 'cf50faa3fe97702ca1ae'
+
+ $('#qunit-fixture').append('<meta name="csrf-token" content="' + correctToken + '"/>')
+
+ currentToken = $.rails.csrfToken()
+
+ start()
+ equal(currentToken, correctToken)
+})
+
+asyncTest('find csrf param', 1, function() {
+ var correctParam = 'authenticity_token'
+
+ $('#qunit-fixture').append('<meta name="csrf-param" content="' + correctParam + '"/>')
+
+ currentParam = $.rails.csrfParam()
+
+ start()
+ equal(currentParam, correctParam)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/data-confirm.js b/actionview/test/ujs/public/test/data-confirm.js
new file mode 100644
index 0000000000..229b9e1466
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-confirm.js
@@ -0,0 +1,316 @@
+module('data-confirm', {
+ setup: function() {
+ $('#qunit-fixture').append($('<a />', {
+ href: '/echo',
+ 'data-remote': 'true',
+ 'data-confirm': 'Are you absolutely sure?',
+ text: 'my social security number'
+ }))
+
+ $('#qunit-fixture').append($('<button />', {
+ 'data-url': '/echo',
+ 'data-remote': 'true',
+ 'data-confirm': 'Are you absolutely sure?',
+ text: 'Click me'
+ }))
+
+ $('#qunit-fixture').append($('<form />', {
+ id: 'confirm',
+ action: '/echo',
+ 'data-remote': 'true'
+ }))
+
+ $('#qunit-fixture').append($('<input />', {
+ type: 'submit',
+ form: 'confirm',
+ 'data-confirm': 'Are you absolutely sure?'
+ }))
+
+ $('#qunit-fixture').append($('<button />', {
+ type: 'submit',
+ form: 'confirm',
+ disabled: 'disabled',
+ 'data-confirm': 'Are you absolutely sure?'
+ }))
+
+ this.windowConfirm = window.confirm
+ },
+ teardown: function() {
+ window.confirm = this.windowConfirm
+ }
+})
+
+asyncTest('clicking on a link with data-confirm attribute. Confirm yes.', 6, function() {
+ var message
+ // auto-confirm:
+ window.confirm = function(msg) { message = msg; 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?')
+ start()
+ })
+ .triggerNative('click')
+})
+
+asyncTest('clicking on a button with data-confirm attribute. Confirm yes.', 6, function() {
+ var message
+ // auto-confirm:
+ window.confirm = function(msg) { message = msg; return true }
+
+ $('button[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?')
+ start()
+ })
+ .triggerNative('click')
+})
+
+asyncTest('clicking on a link with data-confirm attribute. Confirm No.', 3, function() {
+ var message
+ // auto-decline:
+ window.confirm = function(msg) { message = msg; return false }
+
+ $('a[data-confirm]')
+ .bindNative('confirm:complete', function(e, data) {
+ App.assertCallbackInvoked('confirm:complete')
+ ok(data == false, 'confirm:complete passes in confirm answer (false)')
+ })
+ .bindNative('ajax:beforeSend', function(e, data, status, xhr) {
+ App.assertCallbackNotInvoked('ajax:beforeSend')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ equal(message, 'Are you absolutely sure?')
+ start()
+ }, 50)
+})
+
+asyncTest('clicking on a button with data-confirm attribute. Confirm No.', 3, function() {
+ var message
+ // auto-decline:
+ window.confirm = function(msg) { message = msg; return false }
+
+ $('button[data-confirm]')
+ .bindNative('confirm:complete', function(e, data) {
+ App.assertCallbackInvoked('confirm:complete')
+ ok(data == false, 'confirm:complete passes in confirm answer (false)')
+ })
+ .bindNative('ajax:beforeSend', function(e, data, status, xhr) {
+ App.assertCallbackNotInvoked('ajax:beforeSend')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ equal(message, 'Are you absolutely sure?')
+ start()
+ }, 50)
+})
+
+asyncTest('clicking on a button with data-confirm attribute. Confirm error.', 3, function() {
+ var message
+ // auto-decline:
+ window.confirm = function(msg) { message = msg; throw 'some random error' }
+
+ $('button[data-confirm]')
+ .bindNative('confirm:complete', function(e, data) {
+ App.assertCallbackInvoked('confirm:complete')
+ ok(data == false, 'confirm:complete passes in confirm answer (false)')
+ })
+ .bindNative('ajax:beforeSend', function(e, data, status, xhr) {
+ App.assertCallbackNotInvoked('ajax:beforeSend')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ equal(message, 'Are you absolutely sure?')
+ start()
+ }, 50)
+})
+
+asyncTest('clicking on a submit button with form and data-confirm attributes. Confirm No.', 3, function() {
+ var message
+ // auto-decline:
+ window.confirm = function(msg) { message = msg; return false }
+
+ $('input[type=submit][form]')
+ .bindNative('confirm:complete', function(e, data) {
+ App.assertCallbackInvoked('confirm:complete')
+ ok(data == false, 'confirm:complete passes in confirm answer (false)')
+ })
+ .bindNative('ajax:beforeSend', function(e, data, status, xhr) {
+ App.assertCallbackNotInvoked('ajax:beforeSend')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ equal(message, 'Are you absolutely sure?')
+ start()
+ }, 50)
+})
+
+asyncTest('binding to confirm event of a link and returning false', 1, function() {
+ // redefine confirm function so we can make sure it's not called
+ window.confirm = function(msg) {
+ ok(false, 'confirm dialog should not be called')
+ }
+
+ $('a[data-confirm]')
+ .bindNative('confirm', function() {
+ App.assertCallbackInvoked('confirm')
+ return false
+ })
+ .bindNative('confirm:complete', function() {
+ App.assertCallbackNotInvoked('confirm:complete')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ start()
+ }, 50)
+})
+
+asyncTest('binding to confirm event of a button and returning false', 1, function() {
+ // redefine confirm function so we can make sure it's not called
+ window.confirm = function(msg) {
+ ok(false, 'confirm dialog should not be called')
+ }
+
+ $('button[data-confirm]')
+ .bindNative('confirm', function() {
+ App.assertCallbackInvoked('confirm')
+ return false
+ })
+ .bindNative('confirm:complete', function() {
+ App.assertCallbackNotInvoked('confirm:complete')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ start()
+ }, 50)
+})
+
+asyncTest('binding to confirm:complete event of a link and returning false', 2, function() {
+ // auto-confirm:
+ window.confirm = function(msg) {
+ ok(true, 'confirm dialog should be called')
+ return true
+ }
+
+ $('a[data-confirm]')
+ .bindNative('confirm:complete', function() {
+ App.assertCallbackInvoked('confirm:complete')
+ return false
+ })
+ .bindNative('ajax:beforeSend', function() {
+ App.assertCallbackNotInvoked('ajax:beforeSend')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ start()
+ }, 50)
+})
+
+asyncTest('binding to confirm:complete event of a button and returning false', 2, function() {
+ // auto-confirm:
+ window.confirm = function(msg) {
+ ok(true, 'confirm dialog should be called')
+ return true
+ }
+
+ $('button[data-confirm]')
+ .bindNative('confirm:complete', function() {
+ App.assertCallbackInvoked('confirm:complete')
+ return false
+ })
+ .bindNative('ajax:beforeSend', function() {
+ App.assertCallbackNotInvoked('ajax:beforeSend')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ start()
+ }, 50)
+})
+
+asyncTest('a button inside a form only confirms once', 1, function() {
+ var confirmations = 0
+ window.confirm = function(msg) {
+ confirmations++
+ return true
+ }
+
+ $('#qunit-fixture').append($('<form />').append($('<button />', {
+ 'data-remote': 'true',
+ 'data-confirm': 'Are you absolutely sure?',
+ text: 'Click me'
+ })))
+
+ $('form > button[data-confirm]').triggerNative('click')
+
+ ok(confirmations === 1, 'confirmation counter should be 1, but it was ' + confirmations)
+ start()
+})
+
+asyncTest('clicking on the children of a link should also trigger a confirm', 6, function() {
+ var message
+ // auto-confirm:
+ window.confirm = function(msg) { message = msg; return true }
+
+ $('a[data-confirm]')
+ .html('<strong>Click me</strong>')
+ .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?')
+ start()
+ })
+ .find('strong')
+ .triggerNative('click')
+})
+
+asyncTest('clicking on the children of a disabled button should not trigger a confirm.', 1, function() {
+ var message
+ // auto-decline:
+ window.confirm = function(msg) { message = msg; return false }
+
+ $('button[data-confirm][disabled]')
+ .html("<strong>Click me</strong>")
+ .bindNative('confirm', function() {
+ App.assertCallbackNotInvoked('confirm')
+ })
+ .find('strong')
+ .bindNative('click', function() {
+ App.assertCallbackInvoked('click')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ start()
+ }, 50)
+})
diff --git a/actionview/test/ujs/public/test/data-disable-with.js b/actionview/test/ujs/public/test/data-disable-with.js
new file mode 100644
index 0000000000..b29cbbc867
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-disable-with.js
@@ -0,0 +1,391 @@
+module('data-disable-with', {
+ setup: function() {
+ $('#qunit-fixture').append($('<form />', {
+ action: '/echo',
+ 'data-remote': 'true',
+ method: 'post'
+ }))
+ .find('form')
+ .append($('<input type="text" data-disable-with="processing ..." name="user_name" value="john" />'))
+
+ $('#qunit-fixture').append($('<form />', {
+ action: '/echo',
+ method: 'post',
+ id: 'not_remote'
+ }))
+ .find('form:last')
+ // WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!)
+ .append($('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />'))
+
+ $('#qunit-fixture').append($('<a />', {
+ text: 'Click me',
+ href: '/echo',
+ 'data-disable-with': 'clicking...'
+ }))
+
+ $('#qunit-fixture').append($('<input />', {
+ type: 'submit',
+ form: 'not_remote',
+ 'data-disable-with': 'form attr submitting',
+ name: 'submit3',
+ value: 'Form Attr Submit'
+ }))
+
+ $('#qunit-fixture').append($('<button />', {
+ text: 'Click me',
+ 'data-remote': true,
+ 'data-url': '/echo',
+ 'data-disable-with': 'clicking...'
+ }))
+ },
+ teardown: function() {
+ $(document).unbind('iframe:loaded')
+ }
+})
+
+asyncTest('form input field with "data-disable-with" attribute', 7, function() {
+ var form = $('form[data-remote]'), input = form.find('input[type=text]')
+
+ App.checkEnabledState(input, 'john')
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ App.checkEnabledState(input, 'john')
+ equal(data.params.user_name, 'john')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(input, 'processing ...')
+})
+
+asyncTest('blank form input field with "data-disable-with" attribute', 7, function() {
+ var form = $('form[data-remote]'), input = form.find('input[type=text]')
+
+ input.val('')
+ App.checkEnabledState(input, '')
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ App.checkEnabledState(input, '')
+ equal(data.params.user_name, '')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(input, 'processing ...')
+})
+
+asyncTest('form button with "data-disable-with" attribute', 6, function() {
+ var form = $('form[data-remote]'), button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>')
+ form.append(button)
+
+ App.checkEnabledState(button, 'Submit')
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Submit')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(button, 'submitting ...')
+})
+
+asyncTest('form input[type=submit][data-disable-with] disables', 6, function() {
+ var form = $('form:not([data-remote])'), input = form.find('input[type=submit]')
+
+ App.checkEnabledState(input, 'Submit')
+
+ $(document).bind('iframe:loaded', function(e, data) {
+ setTimeout(function() {
+ App.checkDisabledState(input, 'submitting ...')
+ start()
+ }, 30)
+ })
+ form.triggerNative('submit')
+
+ setTimeout(function() {
+ App.checkDisabledState(input, 'submitting ...')
+ }, 30)
+})
+
+test('form input[type=submit][data-disable-with] re-enables when `pageshow` event is triggered', function() {
+ var form = $('form:not([data-remote])'), input = form.find('input[type=submit]')
+
+ App.checkEnabledState(input, 'Submit')
+
+ // Emulate the disabled state without submitting the form at all, what is the
+ // state after going back on firefox after submitting a form.
+ //
+ // See https://github.com/rails/jquery-ujs/issues/357
+ $.rails.disableElement(form[0])
+
+ App.checkDisabledState(input, 'submitting ...')
+
+ $(window).triggerNative('pageshow')
+
+ App.checkEnabledState(input, 'Submit')
+})
+
+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()
+
+ form.bindNative('ajax:success', function() {
+ form.html(origFormContents)
+
+ setTimeout(function() {
+ var input = form.find('input[type=submit]')
+ App.checkEnabledState(input, 'Submit')
+ start()
+ }, 30)
+ }).triggerNative('submit')
+})
+
+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]'),
+ newDisabledInput = input.clone().attr('disabled', 'disabled')
+
+ form.bindNative('ajax:success', function() {
+ input.replaceWith(newDisabledInput)
+
+ setTimeout(function() {
+ App.checkEnabledState(newDisabledInput, 'Submit')
+ start()
+ }, 30)
+ }).triggerNative('submit')
+})
+
+asyncTest('form input[type=submit][data-disable-with] using "form" attribute disables', 6, function() {
+ var form = $('#not_remote'), input = $('input[form=not_remote]')
+ App.checkEnabledState(input, 'Form Attr Submit')
+
+ $(document).bind('iframe:loaded', function(e, data) {
+ setTimeout(function() {
+ App.checkDisabledState(input, 'form attr submitting')
+ start()
+ }, 30)
+ })
+ form.triggerNative('submit')
+
+ setTimeout(function() {
+ App.checkDisabledState(input, 'form attr submitting')
+ }, 30)
+
+})
+
+asyncTest('form[data-remote] textarea[data-disable-with] attribute', 3, function() {
+ var form = $('form[data-remote]'),
+ textarea = $('<textarea data-disable-with="processing ..." name="user_bio">born, lived, died.</textarea>').appendTo(form)
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ equal(data.params.user_bio, 'born, lived, died.')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(textarea, 'processing ...')
+})
+
+asyncTest('a[data-disable-with] disables', 4, function() {
+ var link = $('a[data-disable-with]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click')
+ App.checkDisabledState(link, 'clicking...')
+ start()
+})
+
+test('a[data-disable-with] re-enables when `pageshow` event is triggered', function() {
+ var link = $('a[data-disable-with]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click')
+ App.checkDisabledState(link, 'clicking...')
+
+ $(window).triggerNative('pageshow')
+ App.checkEnabledState(link, 'Click me')
+})
+
+asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, function() {
+ var link = $('a[data-disable-with]').attr('data-remote', true)
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:beforeSend', function() {
+ App.checkDisabledState(link, 'clicking...')
+ })
+ .bindNative('ajax:complete', function() {
+ setTimeout( function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 15)
+ })
+ .triggerNative('click')
+})
+
+asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() {
+ var link = $('a[data-disable-with]').attr('data-remote', true)
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:before', function() {
+ App.checkDisabledState(link, 'clicking...')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() {
+ var link = $('a[data-disable-with]').attr('data-remote', true)
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:beforeSend', function() {
+ App.checkDisabledState(link, 'clicking...')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() {
+ var link = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:beforeSend', function() {
+ App.checkDisabledState(link, 'clicking...')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not disable when `ajax:beforeSend` event is cancelled', 8, function() {
+ var form = $('form[data-remote]'),
+ input = form.find('input:text'),
+ button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>').appendTo(form),
+ textarea = $('<textarea data-disable-with="processing ..." name="user_bio">born, lived, died.</textarea>').appendTo(form),
+ submit = $('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />').appendTo(form)
+
+ form
+ .bindNative('ajax:beforeSend', function() {
+ return false
+ })
+ .triggerNative('submit')
+
+ App.checkEnabledState(input, 'john')
+ App.checkEnabledState(button, 'Submit')
+ App.checkEnabledState(textarea, 'born, lived, died.')
+ App.checkEnabledState(submit, 'Submit')
+
+ start()
+})
+
+asyncTest('ctrl-clicking on a link does not disables the link', 6, function() {
+ var link = $('a[data-disable-with]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { metaKey: true })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { metaKey: true })
+ App.checkEnabledState(link, 'Click me')
+ start()
+})
+
+asyncTest('button[data-remote][data-disable-with] disables and re-enables', 6, function() {
+ var button = $('button[data-remote][data-disable-with]')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(button, 'clicking...')
+ })
+ .bindNative('ajax:complete', function() {
+ setTimeout( function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 15)
+ })
+ .triggerNative('click')
+})
+
+asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() {
+ var button = $('button[data-remote][data-disable-with]')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:before', function() {
+ App.checkDisabledState(button, 'clicking...')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() {
+ var button = $('button[data-remote][data-disable-with]')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:beforeSend', function() {
+ App.checkDisabledState(button, 'clicking...')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() {
+ var button = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(button, 'clicking...')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 30)
+})
diff --git a/actionview/test/ujs/public/test/data-disable.js b/actionview/test/ujs/public/test/data-disable.js
new file mode 100644
index 0000000000..ccc38cf9ae
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-disable.js
@@ -0,0 +1,321 @@
+module('data-disable', {
+ setup: function() {
+ $('#qunit-fixture').append($('<form />', {
+ action: '/echo',
+ 'data-remote': 'true',
+ method: 'post'
+ }))
+ .find('form')
+ .append($('<input type="text" data-disable name="user_name" value="john" />'))
+
+ $('#qunit-fixture').append($('<form />', {
+ action: '/echo',
+ method: 'post'
+ }))
+ .find('form:last')
+ // WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!)
+ .append($('<input type="submit" data-disable name="submit2" value="Submit" />'))
+
+ $('#qunit-fixture').append($('<a />', {
+ text: 'Click me',
+ href: '/echo',
+ 'data-disable': 'true'
+ }))
+
+ $('#qunit-fixture').append($('<button />', {
+ text: 'Click me',
+ 'data-remote': true,
+ 'data-url': '/echo',
+ 'data-disable': 'true'
+ }))
+ },
+ teardown: function() {
+ $(document).unbind('iframe:loaded')
+ }
+})
+
+asyncTest('form input field with "data-disable" attribute', 7, function() {
+ var form = $('form[data-remote]'), input = form.find('input[type=text]')
+
+ App.checkEnabledState(input, 'john')
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ App.checkEnabledState(input, 'john')
+ equal(data.params.user_name, 'john')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(input, 'john')
+})
+
+asyncTest('form button with "data-disable" attribute', 7, function() {
+ var form = $('form[data-remote]'), button = $('<button data-disable name="submit2">Submit</button>')
+ form.append(button)
+
+ App.checkEnabledState(button, 'Submit')
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Submit')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(button, 'Submit')
+ equal(button.data('ujs:enable-with'), undefined)
+})
+
+asyncTest('form input[type=submit][data-disable] disables', 6, function() {
+ var form = $('form:not([data-remote])'), input = form.find('input[type=submit]')
+
+ App.checkEnabledState(input, 'Submit')
+
+ // WEEIRDD: attaching this handler makes the test work in IE7
+ $(document).bind('iframe:loading', function(e, f) {})
+
+ $(document).bind('iframe:loaded', function(e, data) {
+ setTimeout(function() {
+ App.checkDisabledState(input, 'Submit')
+ start()
+ }, 30)
+ })
+ form.triggerNative('submit')
+
+ setTimeout(function() {
+ App.checkDisabledState(input, 'Submit')
+ }, 30)
+})
+
+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()
+
+ form.bindNative('ajax:success', function() {
+ form.html(origFormContents)
+
+ setTimeout(function() {
+ var input = form.find('input[type=submit]')
+ App.checkEnabledState(input, 'Submit')
+ start()
+ }, 30)
+ }).triggerNative('submit')
+})
+
+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]'),
+ newDisabledInput = input.clone().attr('disabled', 'disabled')
+
+ form.bindNative('ajax:success', function() {
+ input.replaceWith(newDisabledInput)
+
+ setTimeout(function() {
+ App.checkEnabledState(newDisabledInput, 'Submit')
+ start()
+ }, 30)
+ }).triggerNative('submit')
+})
+
+asyncTest('form[data-remote] textarea[data-disable] attribute', 3, function() {
+ var form = $('form[data-remote]'),
+ textarea = $('<textarea data-disable name="user_bio">born, lived, died.</textarea>').appendTo(form)
+
+ form.bindNative('ajax:success', function(e, data) {
+ setTimeout(function() {
+ equal(data.params.user_bio, 'born, lived, died.')
+ start()
+ }, 13)
+ })
+ form.triggerNative('submit')
+
+ App.checkDisabledState(textarea, 'born, lived, died.')
+})
+
+asyncTest('a[data-disable] disables', 5, function() {
+ var link = $('a[data-disable]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click')
+ App.checkDisabledState(link, 'Click me')
+ equal(link.data('ujs:enable-with'), undefined)
+ start()
+})
+
+asyncTest('a[data-remote][data-disable] disables and re-enables', 6, function() {
+ var link = $('a[data-disable]').attr('data-remote', true)
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(link, 'Click me')
+ })
+ .bindNative('ajax:complete', function() {
+ setTimeout( function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 15)
+ })
+ .triggerNative('click')
+})
+
+asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() {
+ var link = $('a[data-disable]').attr('data-remote', true)
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:before', function() {
+ App.checkDisabledState(link, 'Click me')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() {
+ var link = $('a[data-disable]').attr('data-remote', true)
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:beforeSend', function() {
+ App.checkDisabledState(link, 'Click me')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('a[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() {
+ var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/error')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(link, 'Click me')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(link, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('form[data-remote] input|button|textarea[data-disable] does not disable when `ajax:beforeSend` event is cancelled', 8, function() {
+ var form = $('form[data-remote]'),
+ input = form.find('input:text'),
+ button = $('<button data-disable="submitting ..." name="submit2">Submit</button>').appendTo(form),
+ textarea = $('<textarea data-disable name="user_bio">born, lived, died.</textarea>').appendTo(form),
+ submit = $('<input type="submit" data-disable="submitting ..." name="submit2" value="Submit" />').appendTo(form)
+
+ form
+ .bindNative('ajax:beforeSend', function() {
+ return false
+ })
+ .triggerNative('submit')
+
+ App.checkEnabledState(input, 'john')
+ App.checkEnabledState(button, 'Submit')
+ App.checkEnabledState(textarea, 'born, lived, died.')
+ App.checkEnabledState(submit, 'Submit')
+
+ start()
+})
+
+asyncTest('ctrl-clicking on a link does not disables the link', 6, function() {
+ var link = $('a[data-disable]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { metaKey: true })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { ctrlKey: true })
+ App.checkEnabledState(link, 'Click me')
+ start()
+})
+
+asyncTest('button[data-remote][data-disable] disables and re-enables', 6, function() {
+ var button = $('button[data-remote][data-disable]')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(button, 'Click me')
+ })
+ .bindNative('ajax:complete', function() {
+ setTimeout( function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 15)
+ })
+ .triggerNative('click')
+})
+
+asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() {
+ var button = $('button[data-remote][data-disable]')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:before', function() {
+ App.checkDisabledState(button, 'Click me')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() {
+ var button = $('button[data-remote][data-disable]')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:beforeSend', function() {
+ App.checkDisabledState(button, 'Click me')
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 30)
+})
+
+asyncTest('button[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() {
+ var button = $('a[data-disable]').attr('data-remote', true).attr('href', '/error')
+
+ App.checkEnabledState(button, 'Click me')
+
+ button
+ .bindNative('ajax:send', function() {
+ App.checkDisabledState(button, 'Click me')
+ })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ App.checkEnabledState(button, 'Click me')
+ start()
+ }, 30)
+})
diff --git a/actionview/test/ujs/public/test/data-method.js b/actionview/test/ujs/public/test/data-method.js
new file mode 100644
index 0000000000..47d940c577
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-method.js
@@ -0,0 +1,85 @@
+(function() {
+
+module('data-method', {
+ setup: function() {
+ $('#qunit-fixture').append($('<a />', {
+ href: '/echo', 'data-method': 'delete', text: 'destroy!'
+ }))
+ },
+ teardown: function() {
+ $(document).unbind('iframe:loaded')
+ }
+})
+
+function submit(fn, options) {
+ $(document).bind('iframe:loaded', function(e, data) {
+ fn(data)
+ start()
+ })
+
+ $('#qunit-fixture').find('a')
+ .triggerNative('click')
+}
+
+asyncTest('link with "data-method" set to "delete"', 3, function() {
+ submit(function(data) {
+ equal(data.REQUEST_METHOD, 'DELETE')
+ strictEqual(data.params.authenticity_token, undefined)
+ strictEqual(data.HTTP_X_CSRF_TOKEN, undefined)
+ })
+})
+
+asyncTest('click on the child of link with "data-method"', 3, function() {
+ $(document).bind('iframe:loaded', function(e, data) {
+ equal(data.REQUEST_METHOD, 'DELETE')
+ strictEqual(data.params.authenticity_token, undefined)
+ strictEqual(data.HTTP_X_CSRF_TOKEN, undefined)
+ start()
+ })
+ $('#qunit-fixture a').html('<strong>destroy!</strong>').find('strong').triggerNative('click')
+})
+
+asyncTest('link with "data-method" and CSRF', 1, function() {
+ $('#qunit-fixture')
+ .append('<meta name="csrf-param" content="authenticity_token"/>')
+ .append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>')
+
+ submit(function(data) {
+ equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae')
+ })
+})
+
+asyncTest('link "target" should be carried over to generated form', 1, function() {
+ $('a[data-method]').attr('target', 'super-special-frame')
+ submit(function(data) {
+ equal(data.params._target, 'super-special-frame')
+ })
+})
+
+asyncTest('link with "data-method" and cross origin', 1, function() {
+ var data = {}
+
+ $('#qunit-fixture')
+ .append('<meta name="csrf-param" content="authenticity_token"/>')
+ .append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>')
+
+ $(document).on('submit', 'form', function(e) {
+ $(e.currentTarget).serializeArray().map(function(item) {
+ data[item.name] = item.value
+ })
+
+ return false
+ })
+
+ var link = $('#qunit-fixture').find('a')
+
+ link.attr('href', 'http://www.alfajango.com')
+
+ link.triggerNative('click')
+
+ start()
+
+ notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae')
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js
new file mode 100644
index 0000000000..b756add24e
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-remote.js
@@ -0,0 +1,415 @@
+module('data-remote', {
+ setup: function() {
+ $('#qunit-fixture')
+ .append($('<a />', {
+ href: '/echo',
+ 'data-remote': 'true',
+ 'data-params': 'data1=value1&data2=value2',
+ text: 'my address'
+ }))
+ .append($('<button />', {
+ 'data-url': '/echo',
+ 'data-remote': 'true',
+ 'data-params': 'data1=value1&data2=value2',
+ text: 'my button'
+ }))
+ .append($('<form />', {
+ action: '/echo',
+ 'data-remote': 'true',
+ method: 'post',
+ id: 'my-remote-form'
+ }))
+ .append($('<a />', {
+ href: '/echo',
+ 'data-remote': 'true',
+ disabled: 'disabled',
+ text: 'Disabed link'
+ }))
+ .find('form').append($('<input type="text" name="user_name" value="john">'))
+
+ }
+})
+
+asyncTest('ctrl-clicking on a link does not fire ajaxyness', 0, function() {
+ var link = $('a[data-remote]')
+
+ // Ideally, we'd setup an iframe to intercept normal link clicks
+ // and add a test to make sure the iframe:loaded event is triggered.
+ // However, jquery doesn't actually cause a native `click` event and
+ // follow links using `trigger('click')`, it only fires bindings.
+ link
+ .removeAttr('data-params')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+
+ link.triggerNative('click', { metaKey: true })
+ link.triggerNative('click', { ctrlKey: true })
+
+ setTimeout(function() { start() }, 13)
+})
+
+asyncTest('ctrl-clicking on a link still fires ajax for non-GET links and for links with "data-params"', 2, function() {
+ var link = $('a[data-remote]')
+
+ link
+ .removeAttr('data-params')
+ .attr('data-method', 'POST')
+ .bindNative('ajax:beforeSend', function() {
+ ok(true, 'ajax should be triggered')
+ })
+ .triggerNative('click', { metaKey: true })
+
+ link
+ .removeAttr('data-method')
+ .attr('data-params', 'name=steve')
+ .triggerNative('click', { metaKey: true })
+
+ setTimeout(function() { start() }, 13)
+})
+
+asyncTest('clicking on a link with data-remote attribute', 5, function() {
+ $('a[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ App.assertRequestPath(data, '/echo')
+ equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
+ equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
+ App.assertGetRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('click')
+})
+
+asyncTest('clicking on a link with both query string in href and data-params', 4, function() {
+ $('a[data-remote]')
+ .attr('href', '/echo?data3=value3')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertGetRequest(data)
+ equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
+ equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
+ equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value')
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('click')
+})
+
+asyncTest('clicking on a link with both query string in href and data-params with POST method', 4, function() {
+ $('a[data-remote]')
+ .attr('href', '/echo?data3=value3')
+ .attr('data-method', 'post')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertPostRequest(data)
+ equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
+ equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
+ equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value')
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('click')
+})
+
+asyncTest('clicking on a link with disabled attribute', 0, function() {
+ $('a[disabled]')
+ .bindNative('ajax:before', function(e, data, status, xhr) {
+ App.assertCallbackNotInvoked('ajax:success')
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('click')
+
+ setTimeout(function() {
+ start()
+ }, 13)
+})
+
+asyncTest('clicking on a button with data-remote attribute', 5, function() {
+ $('button[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ App.assertRequestPath(data, '/echo')
+ equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
+ equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
+ App.assertGetRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('click')
+})
+
+asyncTest('changing a select option with data-remote attribute', 5, function() {
+ $('form')
+ .append(
+ $('<select />', {
+ 'name': 'user_data',
+ 'data-remote': 'true',
+ 'data-params': 'data1=value1',
+ 'data-url': '/echo'
+ })
+ .append($('<option />', {value: 'optionValue1', text: 'option1'}))
+ .append($('<option />', {value: 'optionValue2', text: 'option2'}))
+ )
+
+ $('select[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ App.assertRequestPath(data, '/echo')
+ equal(data.params.user_data, 'optionValue2', 'ajax arguments should have key term with right value')
+ equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
+ App.assertGetRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .val('optionValue2')
+ .triggerNative('change')
+})
+
+asyncTest('submitting form with data-remote attribute', 4, function() {
+ $('form[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ App.assertRequestPath(data, '/echo')
+ equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value')
+ App.assertPostRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('submit')
+})
+
+asyncTest('submitting form with data-remote attribute should include inputs in a fieldset only once', 3, function() {
+ $('form[data-remote]')
+ .append('<fieldset><input name="items[]" value="Item" /></fieldset>')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ equal(data.params.items.length, 1, 'ajax arguments should only have the item once')
+ App.assertPostRequest(data)
+ })
+ .bindNative('ajax:complete', function() {
+ $('form[data-remote], fieldset').remove()
+ start()
+ })
+ .triggerNative('submit')
+})
+
+asyncTest('submitting form with data-remote attribute submits input with matching [form] attribute', 5, function() {
+ $('#qunit-fixture')
+ .append($('<input type="text" name="user_data" value="value1" form="my-remote-form">'))
+
+ $('form[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ 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')
+ App.assertPostRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+ .triggerNative('submit')
+})
+
+asyncTest('submitting form with data-remote attribute by clicking button with matching [form] attribute', 5, function() {
+ $('form[data-remote]')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ App.assertCallbackInvoked('ajax:success')
+ 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, 'value2', 'ajax arguments should have key user_data with right value')
+ App.assertPostRequest(data)
+ })
+ .bindNative('ajax:complete', function() { start() })
+
+ $('<button />', {
+ type: 'submit',
+ name: 'user_data',
+ value: 'value1',
+ form: 'my-remote-form'
+ })
+ .appendTo($('#qunit-fixture'))
+
+ $('<button />', {
+ type: 'submit',
+ name: 'user_data',
+ value: 'value2',
+ form: 'my-remote-form'
+ })
+ .appendTo($('#qunit-fixture'))
+ .triggerNative('click')
+})
+
+asyncTest('form\'s submit bindings in browsers that don\'t support submit bubbling', 5, function() {
+ var form = $('form[data-remote]'), directBindingCalled = false
+
+ ok(!directBindingCalled, 'nothing is called')
+
+ form
+ .append($('<input type="submit" />'))
+ .bindNative('submit', function(event) {
+ ok(event.type == 'submit', 'submit event handlers are called with submit event')
+ ok(true, 'binding handler is called')
+ directBindingCalled = true
+ })
+ .bindNative('ajax:beforeSend', function() {
+ ok(true, 'form being submitted via ajax')
+ ok(directBindingCalled, 'binding handler already called')
+ })
+ .bindNative('ajax:complete', function() {
+ start()
+ })
+
+ if(!$.support.submitBubbles) {
+ // Must indrectly submit form via click to trigger jQuery's manual submit bubbling in IE
+ form.find('input[type=submit]')
+ .triggerNative('click')
+ } else {
+ form.triggerNative('submit')
+ }
+})
+
+asyncTest('returning false in form\'s submit bindings in non-submit-bubbling browsers', 1, function() {
+ var form = $('form[data-remote]')
+
+ form
+ .append($('<input type="submit" />'))
+ .bindNative('submit', function() {
+ ok(true, 'binding handler is called')
+ return false
+ })
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'form should not be submitted')
+ })
+
+ if (!$.support.submitBubbles) {
+ // Must indrectly submit form via click to trigger jQuery's manual submit bubbling in IE
+ form.find('input[type=submit]').triggerNative('click')
+ } else {
+ form.triggerNative('submit')
+ }
+
+ setTimeout(function() { start() }, 13)
+})
+
+asyncTest('clicking on a link with falsy "data-remote" attribute does not fire ajaxyness', 0, function() {
+ $('a[data-remote]')
+ .attr('data-remote', 'false')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+ .bindNative('click', function() {
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() { start() }, 20)
+})
+
+asyncTest('ctrl-clicking on a link with falsy "data-remote" attribute does not fire ajaxyness even if "data-params" present', 0, function() {
+ var link = $('a[data-remote]')
+
+ link
+ .removeAttr('data-params')
+ .attr('data-remote', 'false')
+ .attr('data-method', 'POST')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+ .bindNative('click', function() {
+ return false
+ })
+ .triggerNative('click', { metaKey: true })
+
+ link
+ .removeAttr('data-method')
+ .attr('data-params', 'name=steve')
+ .triggerNative('click', { metaKey: true })
+
+ setTimeout(function() { start() }, 20)
+})
+
+asyncTest('clicking on a button with falsy "data-remote" attribute', 0, function() {
+ $('button[data-remote]:first')
+ .attr('data-remote', 'false')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+ .bindNative('click', function() {
+ return false
+ })
+ .triggerNative('click')
+
+ setTimeout(function() { start() }, 20)
+})
+
+asyncTest('submitting a form with falsy "data-remote" attribute', 0, function() {
+ $('form[data-remote]:first')
+ .attr('data-remote', 'false')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+ .bindNative('submit', function() {
+ return false
+ })
+ .triggerNative('submit')
+
+ setTimeout(function() { start() }, 20)
+})
+
+asyncTest('changing a select option with falsy "data-remote" attribute', 0, function() {
+ $('form')
+ .append(
+ $('<select />', {
+ 'name': 'user_data',
+ 'data-remote': 'false',
+ 'data-params': 'data1=value1',
+ 'data-url': '/echo'
+ })
+ .append($('<option />', {value: 'optionValue1', text: 'option1'}))
+ .append($('<option />', {value: 'optionValue2', text: 'option2'}))
+ )
+
+ $('select[data-remote=false]:first')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+ .val('optionValue2')
+ .triggerNative('change')
+
+ setTimeout(function() { start() }, 20)
+})
+
+asyncTest('form should be serialized correctly', 6, function() {
+ $('form')
+ .append('<textarea name="textarea">textarea</textarea>')
+ .append('<input type="checkbox" name="checkbox[]" value="0" />')
+ .append('<input type="checkbox" checked="checked" name="checkbox[]" value="1" />')
+ .append('<input type="radio" checked="checked" name="radio" value="0" />')
+ .append('<input type="radio" name="radio" value="1" />')
+ .append('<select multiple="multiple" name="select[]">\
+ <option value="1" selected>1</option>\
+ <option value="2" selected>2</option>\
+ <option value="3">3</option>\
+ <option selected>4</option>\
+ </select>')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ equal(data.params.checkbox.length, 1)
+ equal(data.params.checkbox[0], '1')
+ equal(data.params.radio, '0')
+ equal(data.params.select.length, 3)
+ equal(data.params.select[2], '4')
+ equal(data.params.textarea, 'textarea')
+
+ start()
+ })
+ .triggerNative('submit')
+})
+
+asyncTest('form buttons should only be serialized when clicked', 4, function() {
+ $('form')
+ .append('<input type="submit" name="submit1" value="submit1" />')
+ .append('<button name="submit2" value="submit2" />')
+ .append('<button name="submit3" value="submit3" />')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ equal(data.params.submit1, undefined)
+ equal(data.params.submit2, 'submit2')
+ equal(data.params.submit3, undefined)
+ equal(data['rack.request.form_vars'], 'user_name=john&submit2=submit2')
+
+ start()
+ })
+ .find('[name=submit2]').triggerNative('click')
+})
diff --git a/actionview/test/ujs/public/test/override.js b/actionview/test/ujs/public/test/override.js
new file mode 100644
index 0000000000..299c7018cc
--- /dev/null
+++ b/actionview/test/ujs/public/test/override.js
@@ -0,0 +1,56 @@
+(function() {
+
+var realHref
+
+module('override', {
+ setup: function() {
+ realHref = $.rails.href
+ $('#qunit-fixture')
+ .append($('<a />', {
+ href: '/real/href', 'data-remote': 'true', 'data-method': 'delete', 'data-href': '/data/href'
+ }))
+ },
+ teardown: function() {
+ $.rails.href = realHref
+ }
+})
+
+asyncTest('the getter for an element\'s href is publicly accessible', 1, function() {
+ ok($.rails.href)
+ start()
+})
+
+asyncTest('the getter for an element\'s href is overridable', 1, function() {
+ $.rails.href = function(element) { return $(element).data('href') }
+ $('#qunit-fixture a')
+ .bindNative('ajax:beforeSend', function(e, xhr, options) {
+ equal('/data/href', options.url)
+ return false
+ })
+ .triggerNative('click')
+ start()
+})
+
+asyncTest('the getter for an element\'s href works normally if not overridden', 1, function() {
+ $('#qunit-fixture a')
+ .bindNative('ajax:beforeSend', function(e, xhr, options) {
+ equal(location.protocol + '//' + location.host + '/real/href', options.url)
+ return false
+ })
+ .triggerNative('click')
+ start()
+})
+
+asyncTest('the event selector strings are overridable', 1, function() {
+ ok($.rails.linkClickSelector.indexOf(', a[data-custom-remote-link]') != -1, 'linkClickSelector contains custom selector')
+ start()
+})
+
+asyncTest('including rails-ujs multiple times throws error', 1, function() {
+ throws(function() {
+ Rails.start()
+ }, 'appending rails.js again throws error')
+ setTimeout(function() { start() }, 50)
+})
+
+})()
diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js
new file mode 100644
index 0000000000..299c71bb00
--- /dev/null
+++ b/actionview/test/ujs/public/test/settings.js
@@ -0,0 +1,116 @@
+var App = App || {}
+
+App.assertCallbackInvoked = function(callbackName) {
+ ok(true, callbackName + ' callback should have been invoked')
+}
+
+App.assertCallbackNotInvoked = function(callbackName) {
+ ok(false, callbackName + ' callback should not have been invoked')
+}
+
+App.assertGetRequest = function(requestEnv) {
+ equal(requestEnv['REQUEST_METHOD'], 'GET', 'request type should be GET')
+}
+
+App.assertPostRequest = function(requestEnv) {
+ equal(requestEnv['REQUEST_METHOD'], 'POST', 'request type should be POST')
+}
+
+App.assertRequestPath = function(requestEnv, path) {
+ equal(requestEnv['PATH_INFO'], path, 'request should be sent to right url')
+}
+
+App.getVal = function(el) {
+ return el.is('input,textarea,select') ? el.val() : el.text()
+}
+
+App.disabled = function(el) {
+ return el.is('input,textarea,select,button') ?
+ (el.is(':disabled') && $.rails.getData(el[0], 'ujs:disabled')) :
+ $.rails.getData(el[0], 'ujs:disabled')
+}
+
+App.checkEnabledState = function(el, text) {
+ ok(!App.disabled(el), el.get(0).tagName + ' should not be disabled')
+ equal(App.getVal(el), text, el.get(0).tagName + ' text should be original value')
+}
+
+App.checkDisabledState = function(el, text) {
+ ok(App.disabled(el), el.get(0).tagName + ' should be disabled')
+ equal(App.getVal(el), text, el.get(0).tagName + ' text should be disabled value')
+}
+
+// hijacks normal form submit; lets it submit to an iframe to prevent
+// navigating away from the test suite
+$(document).bind('submit', function(e) {
+ if (!e.isDefaultPrevented()) {
+ var form = $(e.target), action = form.attr('action'),
+ name = 'form-frame' + jQuery.guid++,
+ iframe = $('<iframe name="' + name + '" />'),
+ iframeInput = '<input name="iframe" value="true" type="hidden" />'
+ targetInput = '<input name="_target" value="' + (form.attr('target') || '') + '" type="hidden" />'
+
+ if (action && action.indexOf('iframe') < 0) {
+ if (action.indexOf('?') < 0) {
+ form.attr('action', action + '?iframe=true')
+ } else {
+ form.attr('action', action + '&iframe=true')
+ }
+ }
+ form.attr('target', name).append(iframeInput, targetInput)
+ $('#qunit-fixture').append(iframe)
+ $.event.trigger('iframe:loading', form)
+ }
+})
+
+var _MouseEvent = window.MouseEvent
+
+try {
+ new _MouseEvent()
+} catch (e) {
+ _MouseEvent = function(type, options) {
+ var evt = document.createEvent('MouseEvents')
+ evt.initMouseEvent(type, options.bubbles, options.cancelable, window, options.detail, 0, 0, 80, 20, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, null)
+ return evt
+ }
+}
+
+$.fn.extend({
+ // trigger a native click event
+ triggerNative: function(type, options) {
+ var el = this[0],
+ event,
+ Evt = {
+ 'click': _MouseEvent,
+ 'change': Event,
+ 'pageshow': PageTransitionEvent,
+ 'submit': Event
+ }[type]
+
+ options = options || {}
+ options.bubbles = true
+ options.cancelable = true
+
+ event = new Evt(type, options)
+
+ el.dispatchEvent(event)
+
+ if (type === 'submit' && !event.defaultPrevented) {
+ el.submit()
+ }
+ return this
+ },
+ bindNative: function(event, handler) {
+ if (!handler) return this
+
+ this.bind(event, function(e) {
+ var args = []
+ if (e.originalEvent.detail) {
+ args = e.originalEvent.detail.slice()
+ }
+ args.unshift(e)
+ return handler.apply(this, args)
+ })
+ return this
+ }
+})
diff --git a/actionview/test/ujs/public/vendor/jquery.metadata.js b/actionview/test/ujs/public/vendor/jquery.metadata.js
new file mode 100644
index 0000000000..5b5253cdf7
--- /dev/null
+++ b/actionview/test/ujs/public/vendor/jquery.metadata.js
@@ -0,0 +1,122 @@
+/*
+ * Metadata - jQuery plugin for parsing metadata from elements
+ *
+ * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.metadata.js 4187 2007-12-16 17:15:27Z joern.zaefferer $
+ *
+ */
+
+/**
+ * Sets the type of metadata to use. Metadata is encoded in JSON, and each property
+ * in the JSON will become a property of the element itself.
+ *
+ * There are three supported types of metadata storage:
+ *
+ * attr: Inside an attribute. The name parameter indicates *which* attribute.
+ *
+ * class: Inside the class attribute, wrapped in curly braces: { }
+ *
+ * elem: Inside a child element (e.g. a script tag). The
+ * name parameter indicates *which* element.
+ *
+ * The metadata for an element is loaded the first time the element is accessed via jQuery.
+ *
+ * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements
+ * matched by expr, then redefine the metadata type and run another $(expr) for other elements.
+ *
+ * @name $.metadata.setType
+ *
+ * @example <p id="one" class="some_class {item_id: 1, item_label: 'Label'}">This is a p</p>
+ * @before $.metadata.setType("class")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from the class attribute
+ *
+ * @example <p id="one" class="some_class" data="{item_id: 1, item_label: 'Label'}">This is a p</p>
+ * @before $.metadata.setType("attr", "data")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from a "data" attribute
+ *
+ * @example <p id="one" class="some_class"><script>{item_id: 1, item_label: 'Label'}</script>This is a p</p>
+ * @before $.metadata.setType("elem", "script")
+ * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
+ * @desc Reads metadata from a nested script element
+ *
+ * @param String type The encoding type
+ * @param String name The name of the attribute to be used to get metadata (optional)
+ * @cat Plugins/Metadata
+ * @descr Sets the type of encoding to be used when loading metadata for the first time
+ * @type undefined
+ * @see metadata()
+ */
+
+(function($) {
+
+$.extend({
+ metadata : {
+ defaults : {
+ type: 'class',
+ name: 'metadata',
+ cre: /({.*})/,
+ single: 'metadata'
+ },
+ setType: function( type, name ){
+ this.defaults.type = type;
+ this.defaults.name = name;
+ },
+ get: function( elem, opts ){
+ var settings = $.extend({},this.defaults,opts);
+ // check for empty string in single property
+ if ( !settings.single.length ) settings.single = 'metadata';
+
+ var data = $.data(elem, settings.single);
+ // returned cached data if it already exists
+ if ( data ) return data;
+
+ data = "{}";
+
+ if ( settings.type == "class" ) {
+ var m = settings.cre.exec( elem.className );
+ if ( m )
+ data = m[1];
+ } else if ( settings.type == "elem" ) {
+ if( !elem.getElementsByTagName )
+ return undefined;
+ var e = elem.getElementsByTagName(settings.name);
+ if ( e.length )
+ data = $.trim(e[0].innerHTML);
+ } else if ( elem.getAttribute != undefined ) {
+ var attr = elem.getAttribute( settings.name );
+ if ( attr )
+ data = attr;
+ }
+
+ if ( data.indexOf( '{' ) <0 )
+ data = "{" + data + "}";
+
+ data = eval("(" + data + ")");
+
+ $.data( elem, settings.single, data );
+ return data;
+ }
+ }
+});
+
+/**
+ * Returns the metadata object for the first member of the jQuery object.
+ *
+ * @name metadata
+ * @descr Returns element's metadata object
+ * @param Object opts An object containing settings to override the defaults
+ * @type jQuery
+ * @cat Plugins/Metadata
+ */
+$.fn.metadata = function( opts ){
+ return $.metadata.get( this[0], opts );
+};
+
+})(jQuery); \ No newline at end of file
diff --git a/actionview/test/ujs/public/vendor/qunit.css b/actionview/test/ujs/public/vendor/qunit.css
new file mode 100644
index 0000000000..93026e3ba3
--- /dev/null
+++ b/actionview/test/ujs/public/vendor/qunit.css
@@ -0,0 +1,237 @@
+/*!
+ * QUnit 1.14.0
+ * http://qunitjs.com/
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2014-01-31T16:40Z
+ */
+
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+ margin: 0;
+ padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+ padding: 0.5em 0 0.5em 1em;
+
+ color: #8699A4;
+ background-color: #0D3349;
+
+ font-size: 1.5em;
+ line-height: 1em;
+ font-weight: 400;
+
+ border-radius: 5px 5px 0 0;
+}
+
+#qunit-header a {
+ text-decoration: none;
+ color: #C2CCD1;
+}
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+ color: #FFF;
+}
+
+#qunit-testrunner-toolbar label {
+ display: inline-block;
+ padding: 0 0.5em 0 0.1em;
+}
+
+#qunit-banner {
+ height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+ padding: 0.5em 0 0.5em 2em;
+ color: #5E740B;
+ background-color: #EEE;
+ overflow: hidden;
+}
+
+#qunit-userAgent {
+ padding: 0.5em 0 0.5em 2.5em;
+ background-color: #2B81AF;
+ color: #FFF;
+ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+#qunit-modulefilter-container {
+ float: right;
+}
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+ list-style-position: inside;
+}
+
+#qunit-tests li {
+ padding: 0.4em 0.5em 0.4em 2.5em;
+ border-bottom: 1px solid #FFF;
+ list-style-position: inside;
+}
+
+#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
+ display: none;
+}
+
+#qunit-tests li strong {
+ cursor: pointer;
+}
+
+#qunit-tests li a {
+ padding: 0.5em;
+ color: #C2CCD1;
+ text-decoration: none;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+ color: #000;
+}
+
+#qunit-tests li .runtime {
+ float: right;
+ font-size: smaller;
+}
+
+.qunit-assert-list {
+ margin-top: 0.5em;
+ padding: 0.5em;
+
+ background-color: #FFF;
+
+ border-radius: 5px;
+}
+
+.qunit-collapsed {
+ display: none;
+}
+
+#qunit-tests table {
+ border-collapse: collapse;
+ margin-top: 0.2em;
+}
+
+#qunit-tests th {
+ text-align: right;
+ vertical-align: top;
+ padding: 0 0.5em 0 0;
+}
+
+#qunit-tests td {
+ vertical-align: top;
+}
+
+#qunit-tests pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+#qunit-tests del {
+ background-color: #E0F2BE;
+ color: #374E0C;
+ text-decoration: none;
+}
+
+#qunit-tests ins {
+ background-color: #FFCACA;
+ color: #500;
+ text-decoration: none;
+}
+
+/*** Test Counts */
+
+#qunit-tests b.counts { color: #000; }
+#qunit-tests b.passed { color: #5E740B; }
+#qunit-tests b.failed { color: #710909; }
+
+#qunit-tests li li {
+ padding: 5px;
+ background-color: #FFF;
+ border-bottom: none;
+ list-style-position: inside;
+}
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+ color: #3C510C;
+ background-color: #FFF;
+ border-left: 10px solid #C6E746;
+}
+
+#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected { color: #999; }
+
+#qunit-banner.qunit-pass { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+ color: #710909;
+ background-color: #FFF;
+ border-left: 10px solid #EE5757;
+ white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+ border-radius: 0 0 5px 5px;
+}
+
+#qunit-tests .fail { color: #000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name { color: #000; }
+
+#qunit-tests .fail .test-actual { color: #EE5757; }
+#qunit-tests .fail .test-expected { color: #008000; }
+
+#qunit-banner.qunit-fail { background-color: #EE5757; }
+
+
+/** Result */
+
+#qunit-testresult {
+ padding: 0.5em 0.5em 0.5em 2.5em;
+
+ color: #2B81AF;
+ background-color: #D2E0E6;
+
+ border-bottom: 1px solid #FFF;
+}
+#qunit-testresult .module-name {
+ font-weight: 700;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+ position: absolute;
+ top: -10000px;
+ left: -10000px;
+ width: 1000px;
+ height: 1000px;
+}
diff --git a/actionview/test/ujs/public/vendor/qunit.js b/actionview/test/ujs/public/vendor/qunit.js
new file mode 100644
index 0000000000..0e279fde17
--- /dev/null
+++ b/actionview/test/ujs/public/vendor/qunit.js
@@ -0,0 +1,2288 @@
+/*!
+ * QUnit 1.14.0
+ * http://qunitjs.com/
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2014-01-31T16:40Z
+ */
+
+(function( window ) {
+
+var QUnit,
+ assert,
+ config,
+ onErrorFnPrev,
+ testId = 0,
+ fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty,
+ // Keep a local reference to Date (GH-283)
+ Date = window.Date,
+ setTimeout = window.setTimeout,
+ clearTimeout = window.clearTimeout,
+ defined = {
+ document: typeof window.document !== "undefined",
+ setTimeout: typeof window.setTimeout !== "undefined",
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch( e ) {
+ return false;
+ }
+ }())
+ },
+ /**
+ * Provides a normalized error string, correcting an issue
+ * with IE 7 (and prior) where Error.prototype.toString is
+ * not properly implemented
+ *
+ * Based on http://es5.github.com/#x15.11.4.4
+ *
+ * @param {String|Error} error
+ * @return {String} error message
+ */
+ errorString = function( error ) {
+ var name, message,
+ errorString = error.toString();
+ if ( errorString.substring( 0, 7 ) === "[object" ) {
+ name = error.name ? error.name.toString() : "Error";
+ message = error.message ? error.message.toString() : "";
+ if ( name && message ) {
+ return name + ": " + message;
+ } else if ( name ) {
+ return name;
+ } else if ( message ) {
+ return message;
+ } else {
+ return "Error";
+ }
+ } else {
+ return errorString;
+ }
+ },
+ /**
+ * Makes a clone of an object using only Array or Object as base,
+ * and copies over the own enumerable properties.
+ *
+ * @param {Object} obj
+ * @return {Object} New object with only the own properties (recursively).
+ */
+ objectValues = function( obj ) {
+ // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
+ /*jshint newcap: false */
+ var key, val,
+ vals = QUnit.is( "array", obj ) ? [] : {};
+ for ( key in obj ) {
+ if ( hasOwn.call( obj, key ) ) {
+ val = obj[key];
+ vals[key] = val === Object(val) ? objectValues(val) : val;
+ }
+ }
+ return vals;
+ };
+
+
+// Root QUnit object.
+// `QUnit` initialized at top of scope
+QUnit = {
+
+ // call on start of module test to prepend name to all tests
+ module: function( name, testEnvironment ) {
+ config.currentModule = name;
+ config.currentModuleTestEnvironment = testEnvironment;
+ config.modules[name] = true;
+ },
+
+ asyncTest: function( testName, expected, callback ) {
+ if ( arguments.length === 2 ) {
+ callback = expected;
+ expected = null;
+ }
+
+ QUnit.test( testName, expected, callback, true );
+ },
+
+ test: function( testName, expected, callback, async ) {
+ var test,
+ nameHtml = "<span class='test-name'>" + escapeText( testName ) + "</span>";
+
+ if ( arguments.length === 2 ) {
+ callback = expected;
+ expected = null;
+ }
+
+ if ( config.currentModule ) {
+ nameHtml = "<span class='module-name'>" + escapeText( config.currentModule ) + "</span>: " + nameHtml;
+ }
+
+ test = new Test({
+ nameHtml: nameHtml,
+ testName: testName,
+ expected: expected,
+ async: async,
+ callback: callback,
+ module: config.currentModule,
+ moduleTestEnvironment: config.currentModuleTestEnvironment,
+ stack: sourceFromStacktrace( 2 )
+ });
+
+ if ( !validTest( test ) ) {
+ return;
+ }
+
+ test.queue();
+ },
+
+ // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through.
+ expect: function( asserts ) {
+ if (arguments.length === 1) {
+ config.current.expected = asserts;
+ } else {
+ return config.current.expected;
+ }
+ },
+
+ start: function( count ) {
+ // QUnit hasn't been initialized yet.
+ // Note: RequireJS (et al) may delay onLoad
+ if ( config.semaphore === undefined ) {
+ QUnit.begin(function() {
+ // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
+ setTimeout(function() {
+ QUnit.start( count );
+ });
+ });
+ return;
+ }
+
+ config.semaphore -= count || 1;
+ // don't start until equal number of stop-calls
+ if ( config.semaphore > 0 ) {
+ return;
+ }
+ // ignore if start is called more often then stop
+ if ( config.semaphore < 0 ) {
+ config.semaphore = 0;
+ QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
+ return;
+ }
+ // A slight delay, to avoid any current callbacks
+ if ( defined.setTimeout ) {
+ setTimeout(function() {
+ if ( config.semaphore > 0 ) {
+ return;
+ }
+ if ( config.timeout ) {
+ clearTimeout( config.timeout );
+ }
+
+ config.blocking = false;
+ process( true );
+ }, 13);
+ } else {
+ config.blocking = false;
+ process( true );
+ }
+ },
+
+ stop: function( count ) {
+ config.semaphore += count || 1;
+ config.blocking = true;
+
+ if ( config.testTimeout && defined.setTimeout ) {
+ clearTimeout( config.timeout );
+ config.timeout = setTimeout(function() {
+ QUnit.ok( false, "Test timed out" );
+ config.semaphore = 1;
+ QUnit.start();
+ }, config.testTimeout );
+ }
+ }
+};
+
+// We use the prototype to distinguish between properties that should
+// be exposed as globals (and in exports) and those that shouldn't
+(function() {
+ function F() {}
+ F.prototype = QUnit;
+ QUnit = new F();
+ // Make F QUnit's constructor so that we can add to the prototype later
+ QUnit.constructor = F;
+}());
+
+/**
+ * Config object: Maintain internal state
+ * Later exposed as QUnit.config
+ * `config` initialized at top of scope
+ */
+config = {
+ // The queue of tests to run
+ queue: [],
+
+ // block until document ready
+ blocking: true,
+
+ // when enabled, show only failing tests
+ // gets persisted through sessionStorage and can be changed in UI via checkbox
+ hidepassed: false,
+
+ // by default, run previously failed tests first
+ // very useful in combination with "Hide passed tests" checked
+ reorder: true,
+
+ // by default, modify document.title when suite is done
+ altertitle: true,
+
+ // by default, scroll to top of the page when suite is done
+ scrolltop: true,
+
+ // when enabled, all tests must call expect()
+ requireExpects: false,
+
+ // add checkboxes that are persisted in the query-string
+ // when enabled, the id is set to `true` as a `QUnit.config` property
+ urlConfig: [
+ {
+ id: "noglobals",
+ label: "Check for Globals",
+ tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
+ },
+ {
+ id: "notrycatch",
+ label: "No try-catch",
+ tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
+ }
+ ],
+
+ // Set of all modules.
+ modules: {},
+
+ // logging callback queues
+ begin: [],
+ done: [],
+ log: [],
+ testStart: [],
+ testDone: [],
+ moduleStart: [],
+ moduleDone: []
+};
+
+// Initialize more QUnit.config and QUnit.urlParams
+(function() {
+ var i, current,
+ location = window.location || { search: "", protocol: "file:" },
+ params = location.search.slice( 1 ).split( "&" ),
+ length = params.length,
+ urlParams = {};
+
+ if ( params[ 0 ] ) {
+ for ( i = 0; i < length; i++ ) {
+ current = params[ i ].split( "=" );
+ current[ 0 ] = decodeURIComponent( current[ 0 ] );
+
+ // allow just a key to turn on a flag, e.g., test.html?noglobals
+ current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
+ if ( urlParams[ current[ 0 ] ] ) {
+ urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
+ } else {
+ urlParams[ current[ 0 ] ] = current[ 1 ];
+ }
+ }
+ }
+
+ QUnit.urlParams = urlParams;
+
+ // String search anywhere in moduleName+testName
+ config.filter = urlParams.filter;
+
+ // Exact match of the module name
+ config.module = urlParams.module;
+
+ config.testNumber = [];
+ if ( urlParams.testNumber ) {
+
+ // Ensure that urlParams.testNumber is an array
+ urlParams.testNumber = [].concat( urlParams.testNumber );
+ for ( i = 0; i < urlParams.testNumber.length; i++ ) {
+ current = urlParams.testNumber[ i ];
+ config.testNumber.push( parseInt( current, 10 ) );
+ }
+ }
+
+ // Figure out if we're running the tests from a server or not
+ QUnit.isLocal = location.protocol === "file:";
+}());
+
+extend( QUnit, {
+
+ config: config,
+
+ // Initialize the configuration options
+ init: function() {
+ extend( config, {
+ stats: { all: 0, bad: 0 },
+ moduleStats: { all: 0, bad: 0 },
+ started: +new Date(),
+ updateRate: 1000,
+ blocking: false,
+ autostart: true,
+ autorun: false,
+ filter: "",
+ queue: [],
+ semaphore: 1
+ });
+
+ var tests, banner, result,
+ qunit = id( "qunit" );
+
+ if ( qunit ) {
+ qunit.innerHTML =
+ "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+ "<h2 id='qunit-banner'></h2>" +
+ "<div id='qunit-testrunner-toolbar'></div>" +
+ "<h2 id='qunit-userAgent'></h2>" +
+ "<ol id='qunit-tests'></ol>";
+ }
+
+ tests = id( "qunit-tests" );
+ banner = id( "qunit-banner" );
+ result = id( "qunit-testresult" );
+
+ if ( tests ) {
+ tests.innerHTML = "";
+ }
+
+ if ( banner ) {
+ banner.className = "";
+ }
+
+ if ( result ) {
+ result.parentNode.removeChild( result );
+ }
+
+ if ( tests ) {
+ result = document.createElement( "p" );
+ result.id = "qunit-testresult";
+ result.className = "result";
+ tests.parentNode.insertBefore( result, tests );
+ result.innerHTML = "Running...<br/>&nbsp;";
+ }
+ },
+
+ // Resets the test setup. Useful for tests that modify the DOM.
+ /*
+ DEPRECATED: Use multiple tests instead of resetting inside a test.
+ Use testStart or testDone for custom cleanup.
+ This method will throw an error in 2.0, and will be removed in 2.1
+ */
+ reset: function() {
+ var fixture = id( "qunit-fixture" );
+ if ( fixture ) {
+ fixture.innerHTML = config.fixture;
+ }
+ },
+
+ // Safe object type checking
+ is: function( type, obj ) {
+ return QUnit.objectType( obj ) === type;
+ },
+
+ objectType: function( obj ) {
+ if ( typeof obj === "undefined" ) {
+ return "undefined";
+ }
+
+ // Consider: typeof null === object
+ if ( obj === null ) {
+ return "null";
+ }
+
+ var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
+ type = match && match[1] || "";
+
+ switch ( type ) {
+ case "Number":
+ if ( isNaN(obj) ) {
+ return "nan";
+ }
+ return "number";
+ case "String":
+ case "Boolean":
+ case "Array":
+ case "Date":
+ case "RegExp":
+ case "Function":
+ return type.toLowerCase();
+ }
+ if ( typeof obj === "object" ) {
+ return "object";
+ }
+ return undefined;
+ },
+
+ push: function( result, actual, expected, message ) {
+ if ( !config.current ) {
+ throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
+ }
+
+ var output, source,
+ details = {
+ module: config.current.module,
+ name: config.current.testName,
+ result: result,
+ message: message,
+ actual: actual,
+ expected: expected
+ };
+
+ message = escapeText( message ) || ( result ? "okay" : "failed" );
+ message = "<span class='test-message'>" + message + "</span>";
+ output = message;
+
+ if ( !result ) {
+ expected = escapeText( QUnit.jsDump.parse(expected) );
+ actual = escapeText( QUnit.jsDump.parse(actual) );
+ output += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + expected + "</pre></td></tr>";
+
+ if ( actual !== expected ) {
+ output += "<tr class='test-actual'><th>Result: </th><td><pre>" + actual + "</pre></td></tr>";
+ output += "<tr class='test-diff'><th>Diff: </th><td><pre>" + QUnit.diff( expected, actual ) + "</pre></td></tr>";
+ }
+
+ source = sourceFromStacktrace();
+
+ if ( source ) {
+ details.source = source;
+ output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
+ }
+
+ output += "</table>";
+ }
+
+ runLoggingCallbacks( "log", QUnit, details );
+
+ config.current.assertions.push({
+ result: !!result,
+ message: output
+ });
+ },
+
+ pushFailure: function( message, source, actual ) {
+ if ( !config.current ) {
+ throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) );
+ }
+
+ var output,
+ details = {
+ module: config.current.module,
+ name: config.current.testName,
+ result: false,
+ message: message
+ };
+
+ message = escapeText( message ) || "error";
+ message = "<span class='test-message'>" + message + "</span>";
+ output = message;
+
+ output += "<table>";
+
+ if ( actual ) {
+ output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText( actual ) + "</pre></td></tr>";
+ }
+
+ if ( source ) {
+ details.source = source;
+ output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>";
+ }
+
+ output += "</table>";
+
+ runLoggingCallbacks( "log", QUnit, details );
+
+ config.current.assertions.push({
+ result: false,
+ message: output
+ });
+ },
+
+ url: function( params ) {
+ params = extend( extend( {}, QUnit.urlParams ), params );
+ var key,
+ querystring = "?";
+
+ for ( key in params ) {
+ if ( hasOwn.call( params, key ) ) {
+ querystring += encodeURIComponent( key ) + "=" +
+ encodeURIComponent( params[ key ] ) + "&";
+ }
+ }
+ return window.location.protocol + "//" + window.location.host +
+ window.location.pathname + querystring.slice( 0, -1 );
+ },
+
+ extend: extend,
+ id: id,
+ addEvent: addEvent,
+ addClass: addClass,
+ hasClass: hasClass,
+ removeClass: removeClass
+ // load, equiv, jsDump, diff: Attached later
+});
+
+/**
+ * @deprecated: Created for backwards compatibility with test runner that set the hook function
+ * into QUnit.{hook}, instead of invoking it and passing the hook function.
+ * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
+ * Doing this allows us to tell if the following methods have been overwritten on the actual
+ * QUnit object.
+ */
+extend( QUnit.constructor.prototype, {
+
+ // Logging callbacks; all receive a single argument with the listed properties
+ // run test/logs.html for any related changes
+ begin: registerLoggingCallback( "begin" ),
+
+ // done: { failed, passed, total, runtime }
+ done: registerLoggingCallback( "done" ),
+
+ // log: { result, actual, expected, message }
+ log: registerLoggingCallback( "log" ),
+
+ // testStart: { name }
+ testStart: registerLoggingCallback( "testStart" ),
+
+ // testDone: { name, failed, passed, total, runtime }
+ testDone: registerLoggingCallback( "testDone" ),
+
+ // moduleStart: { name }
+ moduleStart: registerLoggingCallback( "moduleStart" ),
+
+ // moduleDone: { name, failed, passed, total }
+ moduleDone: registerLoggingCallback( "moduleDone" )
+});
+
+if ( !defined.document || document.readyState === "complete" ) {
+ config.autorun = true;
+}
+
+QUnit.load = function() {
+ runLoggingCallbacks( "begin", QUnit, {} );
+
+ // Initialize the config, saving the execution queue
+ var banner, filter, i, j, label, len, main, ol, toolbar, val, selection,
+ urlConfigContainer, moduleFilter, userAgent,
+ numModules = 0,
+ moduleNames = [],
+ moduleFilterHtml = "",
+ urlConfigHtml = "",
+ oldconfig = extend( {}, config );
+
+ QUnit.init();
+ extend(config, oldconfig);
+
+ config.blocking = false;
+
+ len = config.urlConfig.length;
+
+ for ( i = 0; i < len; i++ ) {
+ val = config.urlConfig[i];
+ if ( typeof val === "string" ) {
+ val = {
+ id: val,
+ label: val
+ };
+ }
+ config[ val.id ] = QUnit.urlParams[ val.id ];
+ if ( !val.value || typeof val.value === "string" ) {
+ urlConfigHtml += "<input id='qunit-urlconfig-" + escapeText( val.id ) +
+ "' name='" + escapeText( val.id ) +
+ "' type='checkbox'" +
+ ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
+ ( config[ val.id ] ? " checked='checked'" : "" ) +
+ " title='" + escapeText( val.tooltip ) +
+ "'><label for='qunit-urlconfig-" + escapeText( val.id ) +
+ "' title='" + escapeText( val.tooltip ) + "'>" + val.label + "</label>";
+ } else {
+ urlConfigHtml += "<label for='qunit-urlconfig-" + escapeText( val.id ) +
+ "' title='" + escapeText( val.tooltip ) +
+ "'>" + val.label +
+ ": </label><select id='qunit-urlconfig-" + escapeText( val.id ) +
+ "' name='" + escapeText( val.id ) +
+ "' title='" + escapeText( val.tooltip ) +
+ "'><option></option>";
+ selection = false;
+ if ( QUnit.is( "array", val.value ) ) {
+ for ( j = 0; j < val.value.length; j++ ) {
+ urlConfigHtml += "<option value='" + escapeText( val.value[j] ) + "'" +
+ ( config[ val.id ] === val.value[j] ?
+ (selection = true) && " selected='selected'" :
+ "" ) +
+ ">" + escapeText( val.value[j] ) + "</option>";
+ }
+ } else {
+ for ( j in val.value ) {
+ if ( hasOwn.call( val.value, j ) ) {
+ urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
+ ( config[ val.id ] === j ?
+ (selection = true) && " selected='selected'" :
+ "" ) +
+ ">" + escapeText( val.value[j] ) + "</option>";
+ }
+ }
+ }
+ if ( config[ val.id ] && !selection ) {
+ urlConfigHtml += "<option value='" + escapeText( config[ val.id ] ) +
+ "' selected='selected' disabled='disabled'>" +
+ escapeText( config[ val.id ] ) +
+ "</option>";
+ }
+ urlConfigHtml += "</select>";
+ }
+ }
+ for ( i in config.modules ) {
+ if ( config.modules.hasOwnProperty( i ) ) {
+ moduleNames.push(i);
+ }
+ }
+ numModules = moduleNames.length;
+ moduleNames.sort( function( a, b ) {
+ return a.localeCompare( b );
+ });
+ moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " +
+ ( config.module === undefined ? "selected='selected'" : "" ) +
+ ">< All Modules ></option>";
+
+
+ for ( i = 0; i < numModules; i++) {
+ moduleFilterHtml += "<option value='" + escapeText( encodeURIComponent(moduleNames[i]) ) + "' " +
+ ( config.module === moduleNames[i] ? "selected='selected'" : "" ) +
+ ">" + escapeText(moduleNames[i]) + "</option>";
+ }
+ moduleFilterHtml += "</select>";
+
+ // `userAgent` initialized at top of scope
+ userAgent = id( "qunit-userAgent" );
+ if ( userAgent ) {
+ userAgent.innerHTML = navigator.userAgent;
+ }
+
+ // `banner` initialized at top of scope
+ banner = id( "qunit-header" );
+ if ( banner ) {
+ banner.innerHTML = "<a href='" + QUnit.url({ filter: undefined, module: undefined, testNumber: undefined }) + "'>" + banner.innerHTML + "</a> ";
+ }
+
+ // `toolbar` initialized at top of scope
+ toolbar = id( "qunit-testrunner-toolbar" );
+ if ( toolbar ) {
+ // `filter` initialized at top of scope
+ filter = document.createElement( "input" );
+ filter.type = "checkbox";
+ filter.id = "qunit-filter-pass";
+
+ addEvent( filter, "click", function() {
+ var tmp,
+ ol = id( "qunit-tests" );
+
+ if ( filter.checked ) {
+ ol.className = ol.className + " hidepass";
+ } else {
+ tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
+ ol.className = tmp.replace( / hidepass /, " " );
+ }
+ if ( defined.sessionStorage ) {
+ if (filter.checked) {
+ sessionStorage.setItem( "qunit-filter-passed-tests", "true" );
+ } else {
+ sessionStorage.removeItem( "qunit-filter-passed-tests" );
+ }
+ }
+ });
+
+ if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) {
+ filter.checked = true;
+ // `ol` initialized at top of scope
+ ol = id( "qunit-tests" );
+ ol.className = ol.className + " hidepass";
+ }
+ toolbar.appendChild( filter );
+
+ // `label` initialized at top of scope
+ label = document.createElement( "label" );
+ label.setAttribute( "for", "qunit-filter-pass" );
+ label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." );
+ label.innerHTML = "Hide passed tests";
+ toolbar.appendChild( label );
+
+ urlConfigContainer = document.createElement("span");
+ urlConfigContainer.innerHTML = urlConfigHtml;
+ // For oldIE support:
+ // * Add handlers to the individual elements instead of the container
+ // * Use "click" instead of "change" for checkboxes
+ // * Fallback from event.target to event.srcElement
+ addEvents( urlConfigContainer.getElementsByTagName("input"), "click", function( event ) {
+ var params = {},
+ target = event.target || event.srcElement;
+ params[ target.name ] = target.checked ?
+ target.defaultValue || true :
+ undefined;
+ window.location = QUnit.url( params );
+ });
+ addEvents( urlConfigContainer.getElementsByTagName("select"), "change", function( event ) {
+ var params = {},
+ target = event.target || event.srcElement;
+ params[ target.name ] = target.options[ target.selectedIndex ].value || undefined;
+ window.location = QUnit.url( params );
+ });
+ toolbar.appendChild( urlConfigContainer );
+
+ if (numModules > 1) {
+ moduleFilter = document.createElement( "span" );
+ moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
+ moduleFilter.innerHTML = moduleFilterHtml;
+ addEvent( moduleFilter.lastChild, "change", function() {
+ var selectBox = moduleFilter.getElementsByTagName("select")[0],
+ selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
+
+ window.location = QUnit.url({
+ module: ( selectedModule === "" ) ? undefined : selectedModule,
+ // Remove any existing filters
+ filter: undefined,
+ testNumber: undefined
+ });
+ });
+ toolbar.appendChild(moduleFilter);
+ }
+ }
+
+ // `main` initialized at top of scope
+ main = id( "qunit-fixture" );
+ if ( main ) {
+ config.fixture = main.innerHTML;
+ }
+
+ if ( config.autostart ) {
+ QUnit.start();
+ }
+};
+
+if ( defined.document ) {
+ addEvent( window, "load", QUnit.load );
+}
+
+// `onErrorFnPrev` initialized at top of scope
+// Preserve other handlers
+onErrorFnPrev = window.onerror;
+
+// Cover uncaught exceptions
+// Returning true will suppress the default browser handler,
+// returning false will let it run.
+window.onerror = function ( error, filePath, linerNr ) {
+ var ret = false;
+ if ( onErrorFnPrev ) {
+ ret = onErrorFnPrev( error, filePath, linerNr );
+ }
+
+ // Treat return value as window.onerror itself does,
+ // Only do our handling if not suppressed.
+ if ( ret !== true ) {
+ if ( QUnit.config.current ) {
+ if ( QUnit.config.current.ignoreGlobalErrors ) {
+ return true;
+ }
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ } else {
+ QUnit.test( "global failure", extend( function() {
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ }, { validTest: validTest } ) );
+ }
+ return false;
+ }
+
+ return ret;
+};
+
+function done() {
+ config.autorun = true;
+
+ // Log the last module results
+ if ( config.previousModule ) {
+ runLoggingCallbacks( "moduleDone", QUnit, {
+ name: config.previousModule,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all
+ });
+ }
+ delete config.previousModule;
+
+ var i, key,
+ banner = id( "qunit-banner" ),
+ tests = id( "qunit-tests" ),
+ runtime = +new Date() - config.started,
+ passed = config.stats.all - config.stats.bad,
+ html = [
+ "Tests completed in ",
+ runtime,
+ " milliseconds.<br/>",
+ "<span class='passed'>",
+ passed,
+ "</span> assertions of <span class='total'>",
+ config.stats.all,
+ "</span> passed, <span class='failed'>",
+ config.stats.bad,
+ "</span> failed."
+ ].join( "" );
+
+ if ( banner ) {
+ banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" );
+ }
+
+ if ( tests ) {
+ id( "qunit-testresult" ).innerHTML = html;
+ }
+
+ if ( config.altertitle && defined.document && document.title ) {
+ // show ✖ for good, ✔ for bad suite result in title
+ // use escape sequences in case file gets loaded with non-utf-8-charset
+ document.title = [
+ ( config.stats.bad ? "\u2716" : "\u2714" ),
+ document.title.replace( /^[\u2714\u2716] /i, "" )
+ ].join( " " );
+ }
+
+ // clear own sessionStorage items if all tests passed
+ if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
+ // `key` & `i` initialized at top of scope
+ for ( i = 0; i < sessionStorage.length; i++ ) {
+ key = sessionStorage.key( i++ );
+ if ( key.indexOf( "qunit-test-" ) === 0 ) {
+ sessionStorage.removeItem( key );
+ }
+ }
+ }
+
+ // scroll back to top to show results
+ if ( config.scrolltop && window.scrollTo ) {
+ window.scrollTo(0, 0);
+ }
+
+ runLoggingCallbacks( "done", QUnit, {
+ failed: config.stats.bad,
+ passed: passed,
+ total: config.stats.all,
+ runtime: runtime
+ });
+}
+
+/** @return Boolean: true if this test should be ran */
+function validTest( test ) {
+ var include,
+ filter = config.filter && config.filter.toLowerCase(),
+ module = config.module && config.module.toLowerCase(),
+ fullName = ( test.module + ": " + test.testName ).toLowerCase();
+
+ // Internally-generated tests are always valid
+ if ( test.callback && test.callback.validTest === validTest ) {
+ delete test.callback.validTest;
+ return true;
+ }
+
+ if ( config.testNumber.length > 0 ) {
+ if ( inArray( test.testNumber, config.testNumber ) < 0 ) {
+ return false;
+ }
+ }
+
+ if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) {
+ return false;
+ }
+
+ if ( !filter ) {
+ return true;
+ }
+
+ include = filter.charAt( 0 ) !== "!";
+ if ( !include ) {
+ filter = filter.slice( 1 );
+ }
+
+ // If the filter matches, we need to honour include
+ if ( fullName.indexOf( filter ) !== -1 ) {
+ return include;
+ }
+
+ // Otherwise, do the opposite
+ return !include;
+}
+
+// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
+// Later Safari and IE10 are supposed to support error.stack as well
+// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
+function extractStacktrace( e, offset ) {
+ offset = offset === undefined ? 3 : offset;
+
+ var stack, include, i;
+
+ if ( e.stacktrace ) {
+ // Opera
+ return e.stacktrace.split( "\n" )[ offset + 3 ];
+ } else if ( e.stack ) {
+ // Firefox, Chrome
+ stack = e.stack.split( "\n" );
+ if (/^error$/i.test( stack[0] ) ) {
+ stack.shift();
+ }
+ if ( fileName ) {
+ include = [];
+ for ( i = offset; i < stack.length; i++ ) {
+ if ( stack[ i ].indexOf( fileName ) !== -1 ) {
+ break;
+ }
+ include.push( stack[ i ] );
+ }
+ if ( include.length ) {
+ return include.join( "\n" );
+ }
+ }
+ return stack[ offset ];
+ } else if ( e.sourceURL ) {
+ // Safari, PhantomJS
+ // hopefully one day Safari provides actual stacktraces
+ // exclude useless self-reference for generated Error objects
+ if ( /qunit.js$/.test( e.sourceURL ) ) {
+ return;
+ }
+ // for actual exceptions, this is useful
+ return e.sourceURL + ":" + e.line;
+ }
+}
+function sourceFromStacktrace( offset ) {
+ try {
+ throw new Error();
+ } catch ( e ) {
+ return extractStacktrace( e, offset );
+ }
+}
+
+/**
+ * Escape text for attribute or text content.
+ */
+function escapeText( s ) {
+ if ( !s ) {
+ return "";
+ }
+ s = s + "";
+ // Both single quotes and double quotes (for attributes)
+ return s.replace( /['"<>&]/g, function( s ) {
+ switch( s ) {
+ case "'":
+ return "&#039;";
+ case "\"":
+ return "&quot;";
+ case "<":
+ return "&lt;";
+ case ">":
+ return "&gt;";
+ case "&":
+ return "&amp;";
+ }
+ });
+}
+
+function synchronize( callback, last ) {
+ config.queue.push( callback );
+
+ if ( config.autorun && !config.blocking ) {
+ process( last );
+ }
+}
+
+function process( last ) {
+ function next() {
+ process( last );
+ }
+ var start = new Date().getTime();
+ config.depth = config.depth ? config.depth + 1 : 1;
+
+ while ( config.queue.length && !config.blocking ) {
+ if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
+ config.queue.shift()();
+ } else {
+ setTimeout( next, 13 );
+ break;
+ }
+ }
+ config.depth--;
+ if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
+ done();
+ }
+}
+
+function saveGlobal() {
+ config.pollution = [];
+
+ if ( config.noglobals ) {
+ for ( var key in window ) {
+ if ( hasOwn.call( window, key ) ) {
+ // in Opera sometimes DOM element ids show up here, ignore them
+ if ( /^qunit-test-output/.test( key ) ) {
+ continue;
+ }
+ config.pollution.push( key );
+ }
+ }
+ }
+}
+
+function checkPollution() {
+ var newGlobals,
+ deletedGlobals,
+ old = config.pollution;
+
+ saveGlobal();
+
+ newGlobals = diff( config.pollution, old );
+ if ( newGlobals.length > 0 ) {
+ QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
+ }
+
+ deletedGlobals = diff( old, config.pollution );
+ if ( deletedGlobals.length > 0 ) {
+ QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
+ }
+}
+
+// returns a new Array with the elements that are in a but not in b
+function diff( a, b ) {
+ var i, j,
+ result = a.slice();
+
+ for ( i = 0; i < result.length; i++ ) {
+ for ( j = 0; j < b.length; j++ ) {
+ if ( result[i] === b[j] ) {
+ result.splice( i, 1 );
+ i--;
+ break;
+ }
+ }
+ }
+ return result;
+}
+
+function extend( a, b ) {
+ for ( var prop in b ) {
+ if ( hasOwn.call( b, prop ) ) {
+ // Avoid "Member not found" error in IE8 caused by messing with window.constructor
+ if ( !( prop === "constructor" && a === window ) ) {
+ if ( b[ prop ] === undefined ) {
+ delete a[ prop ];
+ } else {
+ a[ prop ] = b[ prop ];
+ }
+ }
+ }
+ }
+
+ return a;
+}
+
+/**
+ * @param {HTMLElement} elem
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvent( elem, type, fn ) {
+ if ( elem.addEventListener ) {
+
+ // Standards-based browsers
+ elem.addEventListener( type, fn, false );
+ } else if ( elem.attachEvent ) {
+
+ // support: IE <9
+ elem.attachEvent( "on" + type, fn );
+ } else {
+
+ // Caller must ensure support for event listeners is present
+ throw new Error( "addEvent() was called in a context without event listener support" );
+ }
+}
+
+/**
+ * @param {Array|NodeList} elems
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvents( elems, type, fn ) {
+ var i = elems.length;
+ while ( i-- ) {
+ addEvent( elems[i], type, fn );
+ }
+}
+
+function hasClass( elem, name ) {
+ return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
+}
+
+function addClass( elem, name ) {
+ if ( !hasClass( elem, name ) ) {
+ elem.className += (elem.className ? " " : "") + name;
+ }
+}
+
+function removeClass( elem, name ) {
+ var set = " " + elem.className + " ";
+ // Class name may appear multiple times
+ while ( set.indexOf(" " + name + " ") > -1 ) {
+ set = set.replace(" " + name + " " , " ");
+ }
+ // If possible, trim it for prettiness, but not necessarily
+ elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, "");
+}
+
+function id( name ) {
+ return defined.document && document.getElementById && document.getElementById( name );
+}
+
+function registerLoggingCallback( key ) {
+ return function( callback ) {
+ config[key].push( callback );
+ };
+}
+
+// Supports deprecated method of completely overwriting logging callbacks
+function runLoggingCallbacks( key, scope, args ) {
+ var i, callbacks;
+ if ( QUnit.hasOwnProperty( key ) ) {
+ QUnit[ key ].call(scope, args );
+ } else {
+ callbacks = config[ key ];
+ for ( i = 0; i < callbacks.length; i++ ) {
+ callbacks[ i ].call( scope, args );
+ }
+ }
+}
+
+// from jquery.js
+function inArray( elem, array ) {
+ if ( array.indexOf ) {
+ return array.indexOf( elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+function Test( settings ) {
+ extend( this, settings );
+ this.assertions = [];
+ this.testNumber = ++Test.count;
+}
+
+Test.count = 0;
+
+Test.prototype = {
+ init: function() {
+ var a, b, li,
+ tests = id( "qunit-tests" );
+
+ if ( tests ) {
+ b = document.createElement( "strong" );
+ b.innerHTML = this.nameHtml;
+
+ // `a` initialized at top of scope
+ a = document.createElement( "a" );
+ a.innerHTML = "Rerun";
+ a.href = QUnit.url({ testNumber: this.testNumber });
+
+ li = document.createElement( "li" );
+ li.appendChild( b );
+ li.appendChild( a );
+ li.className = "running";
+ li.id = this.id = "qunit-test-output" + testId++;
+
+ tests.appendChild( li );
+ }
+ },
+ setup: function() {
+ if (
+ // Emit moduleStart when we're switching from one module to another
+ this.module !== config.previousModule ||
+ // They could be equal (both undefined) but if the previousModule property doesn't
+ // yet exist it means this is the first test in a suite that isn't wrapped in a
+ // module, in which case we'll just emit a moduleStart event for 'undefined'.
+ // Without this, reporters can get testStart before moduleStart which is a problem.
+ !hasOwn.call( config, "previousModule" )
+ ) {
+ if ( hasOwn.call( config, "previousModule" ) ) {
+ runLoggingCallbacks( "moduleDone", QUnit, {
+ name: config.previousModule,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all
+ });
+ }
+ config.previousModule = this.module;
+ config.moduleStats = { all: 0, bad: 0 };
+ runLoggingCallbacks( "moduleStart", QUnit, {
+ name: this.module
+ });
+ }
+
+ config.current = this;
+
+ this.testEnvironment = extend({
+ setup: function() {},
+ teardown: function() {}
+ }, this.moduleTestEnvironment );
+
+ this.started = +new Date();
+ runLoggingCallbacks( "testStart", QUnit, {
+ name: this.testName,
+ module: this.module
+ });
+
+ /*jshint camelcase:false */
+
+
+ /**
+ * Expose the current test environment.
+ *
+ * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
+ */
+ QUnit.current_testEnvironment = this.testEnvironment;
+
+ /*jshint camelcase:true */
+
+ if ( !config.pollution ) {
+ saveGlobal();
+ }
+ if ( config.notrycatch ) {
+ this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
+ return;
+ }
+ try {
+ this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
+ } catch( e ) {
+ QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
+ }
+ },
+ run: function() {
+ config.current = this;
+
+ var running = id( "qunit-testresult" );
+
+ if ( running ) {
+ running.innerHTML = "Running: <br/>" + this.nameHtml;
+ }
+
+ if ( this.async ) {
+ QUnit.stop();
+ }
+
+ this.callbackStarted = +new Date();
+
+ if ( config.notrycatch ) {
+ this.callback.call( this.testEnvironment, QUnit.assert );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ return;
+ }
+
+ try {
+ this.callback.call( this.testEnvironment, QUnit.assert );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ } catch( e ) {
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+
+ QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
+ // else next test will carry the responsibility
+ saveGlobal();
+
+ // Restart the tests if they're blocking
+ if ( config.blocking ) {
+ QUnit.start();
+ }
+ }
+ },
+ teardown: function() {
+ config.current = this;
+ if ( config.notrycatch ) {
+ if ( typeof this.callbackRuntime === "undefined" ) {
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ }
+ this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
+ return;
+ } else {
+ try {
+ this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
+ } catch( e ) {
+ QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
+ }
+ }
+ checkPollution();
+ },
+ finish: function() {
+ config.current = this;
+ if ( config.requireExpects && this.expected === null ) {
+ QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
+ } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
+ QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
+ } else if ( this.expected === null && !this.assertions.length ) {
+ QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
+ }
+
+ var i, assertion, a, b, time, li, ol,
+ test = this,
+ good = 0,
+ bad = 0,
+ tests = id( "qunit-tests" );
+
+ this.runtime = +new Date() - this.started;
+ config.stats.all += this.assertions.length;
+ config.moduleStats.all += this.assertions.length;
+
+ if ( tests ) {
+ ol = document.createElement( "ol" );
+ ol.className = "qunit-assert-list";
+
+ for ( i = 0; i < this.assertions.length; i++ ) {
+ assertion = this.assertions[i];
+
+ li = document.createElement( "li" );
+ li.className = assertion.result ? "pass" : "fail";
+ li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
+ ol.appendChild( li );
+
+ if ( assertion.result ) {
+ good++;
+ } else {
+ bad++;
+ config.stats.bad++;
+ config.moduleStats.bad++;
+ }
+ }
+
+ // store result when possible
+ if ( QUnit.config.reorder && defined.sessionStorage ) {
+ if ( bad ) {
+ sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
+ } else {
+ sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
+ }
+ }
+
+ if ( bad === 0 ) {
+ addClass( ol, "qunit-collapsed" );
+ }
+
+ // `b` initialized at top of scope
+ b = document.createElement( "strong" );
+ b.innerHTML = this.nameHtml + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
+
+ addEvent(b, "click", function() {
+ var next = b.parentNode.lastChild,
+ collapsed = hasClass( next, "qunit-collapsed" );
+ ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
+ });
+
+ addEvent(b, "dblclick", function( e ) {
+ var target = e && e.target ? e.target : window.event.srcElement;
+ if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
+ target = target.parentNode;
+ }
+ if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
+ window.location = QUnit.url({ testNumber: test.testNumber });
+ }
+ });
+
+ // `time` initialized at top of scope
+ time = document.createElement( "span" );
+ time.className = "runtime";
+ time.innerHTML = this.runtime + " ms";
+
+ // `li` initialized at top of scope
+ li = id( this.id );
+ li.className = bad ? "fail" : "pass";
+ li.removeChild( li.firstChild );
+ a = li.firstChild;
+ li.appendChild( b );
+ li.appendChild( a );
+ li.appendChild( time );
+ li.appendChild( ol );
+
+ } else {
+ for ( i = 0; i < this.assertions.length; i++ ) {
+ if ( !this.assertions[i].result ) {
+ bad++;
+ config.stats.bad++;
+ config.moduleStats.bad++;
+ }
+ }
+ }
+
+ runLoggingCallbacks( "testDone", QUnit, {
+ name: this.testName,
+ module: this.module,
+ failed: bad,
+ passed: this.assertions.length - bad,
+ total: this.assertions.length,
+ runtime: this.runtime,
+ // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
+ duration: this.runtime
+ });
+
+ QUnit.reset();
+
+ config.current = undefined;
+ },
+
+ queue: function() {
+ var bad,
+ test = this;
+
+ synchronize(function() {
+ test.init();
+ });
+ function run() {
+ // each of these can by async
+ synchronize(function() {
+ test.setup();
+ });
+ synchronize(function() {
+ test.run();
+ });
+ synchronize(function() {
+ test.teardown();
+ });
+ synchronize(function() {
+ test.finish();
+ });
+ }
+
+ // `bad` initialized at top of scope
+ // defer when previous test run passed, if storage is available
+ bad = QUnit.config.reorder && defined.sessionStorage &&
+ +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
+
+ if ( bad ) {
+ run();
+ } else {
+ synchronize( run, true );
+ }
+ }
+};
+
+// `assert` initialized at top of scope
+// Assert helpers
+// All of these must either call QUnit.push() or manually do:
+// - runLoggingCallbacks( "log", .. );
+// - config.current.assertions.push({ .. });
+assert = QUnit.assert = {
+ /**
+ * Asserts rough true-ish result.
+ * @name ok
+ * @function
+ * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
+ */
+ ok: function( result, msg ) {
+ if ( !config.current ) {
+ throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
+ }
+ result = !!result;
+ msg = msg || ( result ? "okay" : "failed" );
+
+ var source,
+ details = {
+ module: config.current.module,
+ name: config.current.testName,
+ result: result,
+ message: msg
+ };
+
+ msg = "<span class='test-message'>" + escapeText( msg ) + "</span>";
+
+ if ( !result ) {
+ source = sourceFromStacktrace( 2 );
+ if ( source ) {
+ details.source = source;
+ msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" +
+ escapeText( source ) +
+ "</pre></td></tr></table>";
+ }
+ }
+ runLoggingCallbacks( "log", QUnit, details );
+ config.current.assertions.push({
+ result: result,
+ message: msg
+ });
+ },
+
+ /**
+ * Assert that the first two arguments are equal, with an optional message.
+ * Prints out both actual and expected values.
+ * @name equal
+ * @function
+ * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
+ */
+ equal: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ QUnit.push( expected == actual, actual, expected, message );
+ },
+
+ /**
+ * @name notEqual
+ * @function
+ */
+ notEqual: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ QUnit.push( expected != actual, actual, expected, message );
+ },
+
+ /**
+ * @name propEqual
+ * @function
+ */
+ propEqual: function( actual, expected, message ) {
+ actual = objectValues(actual);
+ expected = objectValues(expected);
+ QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name notPropEqual
+ * @function
+ */
+ notPropEqual: function( actual, expected, message ) {
+ actual = objectValues(actual);
+ expected = objectValues(expected);
+ QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name deepEqual
+ * @function
+ */
+ deepEqual: function( actual, expected, message ) {
+ QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name notDeepEqual
+ * @function
+ */
+ notDeepEqual: function( actual, expected, message ) {
+ QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name strictEqual
+ * @function
+ */
+ strictEqual: function( actual, expected, message ) {
+ QUnit.push( expected === actual, actual, expected, message );
+ },
+
+ /**
+ * @name notStrictEqual
+ * @function
+ */
+ notStrictEqual: function( actual, expected, message ) {
+ QUnit.push( expected !== actual, actual, expected, message );
+ },
+
+ "throws": function( block, expected, message ) {
+ var actual,
+ expectedOutput = expected,
+ ok = false;
+
+ // 'expected' is optional
+ if ( !message && typeof expected === "string" ) {
+ message = expected;
+ expected = null;
+ }
+
+ config.current.ignoreGlobalErrors = true;
+ try {
+ block.call( config.current.testEnvironment );
+ } catch (e) {
+ actual = e;
+ }
+ config.current.ignoreGlobalErrors = false;
+
+ if ( actual ) {
+
+ // we don't want to validate thrown error
+ if ( !expected ) {
+ ok = true;
+ expectedOutput = null;
+
+ // expected is an Error object
+ } else if ( expected instanceof Error ) {
+ ok = actual instanceof Error &&
+ actual.name === expected.name &&
+ actual.message === expected.message;
+
+ // expected is a regexp
+ } else if ( QUnit.objectType( expected ) === "regexp" ) {
+ ok = expected.test( errorString( actual ) );
+
+ // expected is a string
+ } else if ( QUnit.objectType( expected ) === "string" ) {
+ ok = expected === errorString( actual );
+
+ // expected is a constructor
+ } else if ( actual instanceof expected ) {
+ ok = true;
+
+ // expected is a validation function which returns true is validation passed
+ } else if ( expected.call( {}, actual ) === true ) {
+ expectedOutput = null;
+ ok = true;
+ }
+
+ QUnit.push( ok, actual, expectedOutput, message );
+ } else {
+ QUnit.pushFailure( message, null, "No exception was thrown." );
+ }
+ }
+};
+
+/**
+ * @deprecated since 1.8.0
+ * Kept assertion helpers in root for backwards compatibility.
+ */
+extend( QUnit.constructor.prototype, assert );
+
+/**
+ * @deprecated since 1.9.0
+ * Kept to avoid TypeErrors for undefined methods.
+ */
+QUnit.constructor.prototype.raises = function() {
+ QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" );
+};
+
+/**
+ * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
+ * Kept to avoid TypeErrors for undefined methods.
+ */
+QUnit.constructor.prototype.equals = function() {
+ QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
+};
+QUnit.constructor.prototype.same = function() {
+ QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
+};
+
+// Test for equality any JavaScript type.
+// Author: Philippe Rathé <prathe@gmail.com>
+QUnit.equiv = (function() {
+
+ // Call the o related callback with the given arguments.
+ function bindCallbacks( o, callbacks, args ) {
+ var prop = QUnit.objectType( o );
+ if ( prop ) {
+ if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
+ return callbacks[ prop ].apply( callbacks, args );
+ } else {
+ return callbacks[ prop ]; // or undefined
+ }
+ }
+ }
+
+ // the real equiv function
+ var innerEquiv,
+ // stack to decide between skip/abort functions
+ callers = [],
+ // stack to avoiding loops from circular referencing
+ parents = [],
+ parentsB = [],
+
+ getProto = Object.getPrototypeOf || function ( obj ) {
+ /*jshint camelcase:false */
+ return obj.__proto__;
+ },
+ callbacks = (function () {
+
+ // for string, boolean, number and null
+ function useStrictEquality( b, a ) {
+ /*jshint eqeqeq:false */
+ if ( b instanceof a.constructor || a instanceof b.constructor ) {
+ // to catch short annotation VS 'new' annotation of a
+ // declaration
+ // e.g. var i = 1;
+ // var j = new Number(1);
+ return a == b;
+ } else {
+ return a === b;
+ }
+ }
+
+ return {
+ "string": useStrictEquality,
+ "boolean": useStrictEquality,
+ "number": useStrictEquality,
+ "null": useStrictEquality,
+ "undefined": useStrictEquality,
+
+ "nan": function( b ) {
+ return isNaN( b );
+ },
+
+ "date": function( b, a ) {
+ return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
+ },
+
+ "regexp": function( b, a ) {
+ return QUnit.objectType( b ) === "regexp" &&
+ // the regex itself
+ a.source === b.source &&
+ // and its modifiers
+ a.global === b.global &&
+ // (gmi) ...
+ a.ignoreCase === b.ignoreCase &&
+ a.multiline === b.multiline &&
+ a.sticky === b.sticky;
+ },
+
+ // - skip when the property is a method of an instance (OOP)
+ // - abort otherwise,
+ // initial === would have catch identical references anyway
+ "function": function() {
+ var caller = callers[callers.length - 1];
+ return caller !== Object && typeof caller !== "undefined";
+ },
+
+ "array": function( b, a ) {
+ var i, j, len, loop, aCircular, bCircular;
+
+ // b could be an object literal here
+ if ( QUnit.objectType( b ) !== "array" ) {
+ return false;
+ }
+
+ len = a.length;
+ if ( len !== b.length ) {
+ // safe and faster
+ return false;
+ }
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+ for ( i = 0; i < len; i++ ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[j] === a[i];
+ bCircular = parentsB[j] === b[i];
+ if ( aCircular || bCircular ) {
+ if ( a[i] === b[i] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ }
+ if ( !loop && !innerEquiv(a[i], b[i]) ) {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ parents.pop();
+ parentsB.pop();
+ return true;
+ },
+
+ "object": function( b, a ) {
+ /*jshint forin:false */
+ var i, j, loop, aCircular, bCircular,
+ // Default to true
+ eq = true,
+ aProperties = [],
+ bProperties = [];
+
+ // comparing constructors is more strict than using
+ // instanceof
+ if ( a.constructor !== b.constructor ) {
+ // Allow objects with no prototype to be equivalent to
+ // objects with Object as their constructor.
+ if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
+ ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
+ return false;
+ }
+ }
+
+ // stack constructor before traversing properties
+ callers.push( a.constructor );
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+
+ // be strict: don't ensure hasOwnProperty and go deep
+ for ( i in a ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[j] === a[i];
+ bCircular = parentsB[j] === b[i];
+ if ( aCircular || bCircular ) {
+ if ( a[i] === b[i] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ eq = false;
+ break;
+ }
+ }
+ }
+ aProperties.push(i);
+ if ( !loop && !innerEquiv(a[i], b[i]) ) {
+ eq = false;
+ break;
+ }
+ }
+
+ parents.pop();
+ parentsB.pop();
+ callers.pop(); // unstack, we are done
+
+ for ( i in b ) {
+ bProperties.push( i ); // collect b's properties
+ }
+
+ // Ensures identical properties name
+ return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
+ }
+ };
+ }());
+
+ innerEquiv = function() { // can take multiple arguments
+ var args = [].slice.apply( arguments );
+ if ( args.length < 2 ) {
+ return true; // end transition
+ }
+
+ return (function( a, b ) {
+ if ( a === b ) {
+ return true; // catch the most you can
+ } else if ( a === null || b === null || typeof a === "undefined" ||
+ typeof b === "undefined" ||
+ QUnit.objectType(a) !== QUnit.objectType(b) ) {
+ return false; // don't lose time with error prone cases
+ } else {
+ return bindCallbacks(a, callbacks, [ b, a ]);
+ }
+
+ // apply transition with (1..n) arguments
+ }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) );
+ };
+
+ return innerEquiv;
+}());
+
+/**
+ * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
+ * http://flesler.blogspot.com Licensed under BSD
+ * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
+ *
+ * @projectDescription Advanced and extensible data dumping for Javascript.
+ * @version 1.0.0
+ * @author Ariel Flesler
+ * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ */
+QUnit.jsDump = (function() {
+ function quote( str ) {
+ return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
+ }
+ function literal( o ) {
+ return o + "";
+ }
+ function join( pre, arr, post ) {
+ var s = jsDump.separator(),
+ base = jsDump.indent(),
+ inner = jsDump.indent(1);
+ if ( arr.join ) {
+ arr = arr.join( "," + s + inner );
+ }
+ if ( !arr ) {
+ return pre + post;
+ }
+ return [ pre, inner + arr, base + post ].join(s);
+ }
+ function array( arr, stack ) {
+ var i = arr.length, ret = new Array(i);
+ this.up();
+ while ( i-- ) {
+ ret[i] = this.parse( arr[i] , undefined , stack);
+ }
+ this.down();
+ return join( "[", ret, "]" );
+ }
+
+ var reName = /^function (\w+)/,
+ jsDump = {
+ // type is used mostly internally, you can fix a (custom)type in advance
+ parse: function( obj, type, stack ) {
+ stack = stack || [ ];
+ var inStack, res,
+ parser = this.parsers[ type || this.typeOf(obj) ];
+
+ type = typeof parser;
+ inStack = inArray( obj, stack );
+
+ if ( inStack !== -1 ) {
+ return "recursion(" + (inStack - stack.length) + ")";
+ }
+ if ( type === "function" ) {
+ stack.push( obj );
+ res = parser.call( this, obj, stack );
+ stack.pop();
+ return res;
+ }
+ return ( type === "string" ) ? parser : this.parsers.error;
+ },
+ typeOf: function( obj ) {
+ var type;
+ if ( obj === null ) {
+ type = "null";
+ } else if ( typeof obj === "undefined" ) {
+ type = "undefined";
+ } else if ( QUnit.is( "regexp", obj) ) {
+ type = "regexp";
+ } else if ( QUnit.is( "date", obj) ) {
+ type = "date";
+ } else if ( QUnit.is( "function", obj) ) {
+ type = "function";
+ } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) {
+ type = "window";
+ } else if ( obj.nodeType === 9 ) {
+ type = "document";
+ } else if ( obj.nodeType ) {
+ type = "node";
+ } else if (
+ // native arrays
+ toString.call( obj ) === "[object Array]" ||
+ // NodeList objects
+ ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
+ ) {
+ type = "array";
+ } else if ( obj.constructor === Error.prototype.constructor ) {
+ type = "error";
+ } else {
+ type = typeof obj;
+ }
+ return type;
+ },
+ separator: function() {
+ return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? "&nbsp;" : " ";
+ },
+ // extra can be a number, shortcut for increasing-calling-decreasing
+ indent: function( extra ) {
+ if ( !this.multiline ) {
+ return "";
+ }
+ var chr = this.indentChar;
+ if ( this.HTML ) {
+ chr = chr.replace( /\t/g, " " ).replace( / /g, "&nbsp;" );
+ }
+ return new Array( this.depth + ( extra || 0 ) ).join(chr);
+ },
+ up: function( a ) {
+ this.depth += a || 1;
+ },
+ down: function( a ) {
+ this.depth -= a || 1;
+ },
+ setParser: function( name, parser ) {
+ this.parsers[name] = parser;
+ },
+ // The next 3 are exposed so you can use them
+ quote: quote,
+ literal: literal,
+ join: join,
+ //
+ depth: 1,
+ // This is the list of parsers, to modify them, use jsDump.setParser
+ parsers: {
+ window: "[Window]",
+ document: "[Document]",
+ error: function(error) {
+ return "Error(\"" + error.message + "\")";
+ },
+ unknown: "[Unknown]",
+ "null": "null",
+ "undefined": "undefined",
+ "function": function( fn ) {
+ var ret = "function",
+ // functions never have name in IE
+ name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
+
+ if ( name ) {
+ ret += " " + name;
+ }
+ ret += "( ";
+
+ ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" );
+ return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" );
+ },
+ array: array,
+ nodelist: array,
+ "arguments": array,
+ object: function( map, stack ) {
+ /*jshint forin:false */
+ var ret = [ ], keys, key, val, i;
+ QUnit.jsDump.up();
+ keys = [];
+ for ( key in map ) {
+ keys.push( key );
+ }
+ keys.sort();
+ for ( i = 0; i < keys.length; i++ ) {
+ key = keys[ i ];
+ val = map[ key ];
+ ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) );
+ }
+ QUnit.jsDump.down();
+ return join( "{", ret, "}" );
+ },
+ node: function( node ) {
+ var len, i, val,
+ open = QUnit.jsDump.HTML ? "&lt;" : "<",
+ close = QUnit.jsDump.HTML ? "&gt;" : ">",
+ tag = node.nodeName.toLowerCase(),
+ ret = open + tag,
+ attrs = node.attributes;
+
+ if ( attrs ) {
+ for ( i = 0, len = attrs.length; i < len; i++ ) {
+ val = attrs[i].nodeValue;
+ // IE6 includes all attributes in .attributes, even ones not explicitly set.
+ // Those have values like undefined, null, 0, false, "" or "inherit".
+ if ( val && val !== "inherit" ) {
+ ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
+ }
+ }
+ }
+ ret += close;
+
+ // Show content of TextNode or CDATASection
+ if ( node.nodeType === 3 || node.nodeType === 4 ) {
+ ret += node.nodeValue;
+ }
+
+ return ret + open + "/" + tag + close;
+ },
+ // function calls it internally, it's the arguments part of the function
+ functionArgs: function( fn ) {
+ var args,
+ l = fn.length;
+
+ if ( !l ) {
+ return "";
+ }
+
+ args = new Array(l);
+ while ( l-- ) {
+ // 97 is 'a'
+ args[l] = String.fromCharCode(97+l);
+ }
+ return " " + args.join( ", " ) + " ";
+ },
+ // object calls it internally, the key part of an item in a map
+ key: quote,
+ // function calls it internally, it's the content of the function
+ functionCode: "[code]",
+ // node calls it internally, it's an html attribute value
+ attribute: quote,
+ string: quote,
+ date: quote,
+ regexp: literal,
+ number: literal,
+ "boolean": literal
+ },
+ // if true, entities are escaped ( <, >, \t, space and \n )
+ HTML: false,
+ // indentation unit
+ indentChar: " ",
+ // if true, items in a collection, are separated by a \n, else just a space.
+ multiline: true
+ };
+
+ return jsDump;
+}());
+
+/*
+ * Javascript Diff Algorithm
+ * By John Resig (http://ejohn.org/)
+ * Modified by Chu Alan "sprite"
+ *
+ * Released under the MIT license.
+ *
+ * More Info:
+ * http://ejohn.org/projects/javascript-diff-algorithm/
+ *
+ * Usage: QUnit.diff(expected, actual)
+ *
+ * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over"
+ */
+QUnit.diff = (function() {
+ /*jshint eqeqeq:false, eqnull:true */
+ function diff( o, n ) {
+ var i,
+ ns = {},
+ os = {};
+
+ for ( i = 0; i < n.length; i++ ) {
+ if ( !hasOwn.call( ns, n[i] ) ) {
+ ns[ n[i] ] = {
+ rows: [],
+ o: null
+ };
+ }
+ ns[ n[i] ].rows.push( i );
+ }
+
+ for ( i = 0; i < o.length; i++ ) {
+ if ( !hasOwn.call( os, o[i] ) ) {
+ os[ o[i] ] = {
+ rows: [],
+ n: null
+ };
+ }
+ os[ o[i] ].rows.push( i );
+ }
+
+ for ( i in ns ) {
+ if ( hasOwn.call( ns, i ) ) {
+ if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
+ n[ ns[i].rows[0] ] = {
+ text: n[ ns[i].rows[0] ],
+ row: os[i].rows[0]
+ };
+ o[ os[i].rows[0] ] = {
+ text: o[ os[i].rows[0] ],
+ row: ns[i].rows[0]
+ };
+ }
+ }
+ }
+
+ for ( i = 0; i < n.length - 1; i++ ) {
+ if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
+ n[ i + 1 ] == o[ n[i].row + 1 ] ) {
+
+ n[ i + 1 ] = {
+ text: n[ i + 1 ],
+ row: n[i].row + 1
+ };
+ o[ n[i].row + 1 ] = {
+ text: o[ n[i].row + 1 ],
+ row: i + 1
+ };
+ }
+ }
+
+ for ( i = n.length - 1; i > 0; i-- ) {
+ if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
+ n[ i - 1 ] == o[ n[i].row - 1 ]) {
+
+ n[ i - 1 ] = {
+ text: n[ i - 1 ],
+ row: n[i].row - 1
+ };
+ o[ n[i].row - 1 ] = {
+ text: o[ n[i].row - 1 ],
+ row: i - 1
+ };
+ }
+ }
+
+ return {
+ o: o,
+ n: n
+ };
+ }
+
+ return function( o, n ) {
+ o = o.replace( /\s+$/, "" );
+ n = n.replace( /\s+$/, "" );
+
+ var i, pre,
+ str = "",
+ out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ),
+ oSpace = o.match(/\s+/g),
+ nSpace = n.match(/\s+/g);
+
+ if ( oSpace == null ) {
+ oSpace = [ " " ];
+ }
+ else {
+ oSpace.push( " " );
+ }
+
+ if ( nSpace == null ) {
+ nSpace = [ " " ];
+ }
+ else {
+ nSpace.push( " " );
+ }
+
+ if ( out.n.length === 0 ) {
+ for ( i = 0; i < out.o.length; i++ ) {
+ str += "<del>" + out.o[i] + oSpace[i] + "</del>";
+ }
+ }
+ else {
+ if ( out.n[0].text == null ) {
+ for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) {
+ str += "<del>" + out.o[n] + oSpace[n] + "</del>";
+ }
+ }
+
+ for ( i = 0; i < out.n.length; i++ ) {
+ if (out.n[i].text == null) {
+ str += "<ins>" + out.n[i] + nSpace[i] + "</ins>";
+ }
+ else {
+ // `pre` initialized at top of scope
+ pre = "";
+
+ for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
+ pre += "<del>" + out.o[n] + oSpace[n] + "</del>";
+ }
+ str += " " + out.n[i].text + nSpace[i] + pre;
+ }
+ }
+ }
+
+ return str;
+ };
+}());
+
+// For browser, export only select globals
+if ( typeof window !== "undefined" ) {
+ extend( window, QUnit.constructor.prototype );
+ window.QUnit = QUnit;
+}
+
+// For CommonJS environments, export everything
+if ( typeof module !== "undefined" && module.exports ) {
+ module.exports = QUnit;
+}
+
+
+// Get a reference to the global object, like window in browsers
+}( (function() {
+ return this;
+})() ));
diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb
new file mode 100644
index 0000000000..7deb208af0
--- /dev/null
+++ b/actionview/test/ujs/server.rb
@@ -0,0 +1,72 @@
+require "rack"
+require "rails"
+require "action_controller/railtie"
+require "action_view/railtie"
+require "blade"
+require "json"
+
+module UJS
+ class Server < Rails::Application
+ routes.append do
+ get "/rails-ujs.js" => Blade::Assets.environment
+ get "/" => "tests#index"
+ match "/echo" => "tests#echo", via: :all
+ get "/error" => proc { |env| [403, {}, []] }
+ end
+
+ config.cache_classes = false
+ config.eager_load = false
+ config.secret_key_base = "59d7a4dbd349fa3838d79e330e39690fc22b931e7dc17d9162f03d633d526fbb92dfdb2dc9804c8be3e199631b9c1fbe43fc3e4fc75730b515851849c728d5c7"
+ config.paths["app/views"].unshift("#{Rails.root}/views")
+ config.public_file_server.enabled = true
+ config.logger = Logger.new(STDOUT)
+ config.log_level = :error
+ 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
+
+ def script_tag(src)
+ src = "/test/#{src}.js" unless src.index("/")
+ %(<script src="#{src}" type="text/javascript"></script>).html_safe
+ end
+end
+
+class TestsController < ActionController::Base
+ helper TestsHelper
+ layout "application"
+
+ def index
+ render :index
+ end
+
+ def echo
+ data = { params: params.to_unsafe_h }.update(request.env)
+
+ if params[:content_type] && params[:content]
+ render inline: params[:content], content_type: params[:content_type]
+ elsif request.xhr?
+ render json: JSON.generate(data)
+ elsif params[:iframe]
+ payload = JSON.generate(data).gsub("<", "&lt;").gsub(">", "&gt;")
+ html = <<-HTML
+ <script>
+ if (window.top && window.top !== window)
+ window.top.jQuery.event.trigger('iframe:loaded', #{payload})
+ </script>
+ <p>You shouldn't be seeing this. <a href="#{request.env['HTTP_REFERER']}">Go back</a></p>
+ HTML
+
+ render html: html.html_safe
+ else
+ render plain: "ERROR: #{request.path} requested without ajax", status: 404
+ end
+ end
+end
+
+Blade.initialize!
+UJS::Server.initialize!
diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb
new file mode 100644
index 0000000000..a69cd2d739
--- /dev/null
+++ b/actionview/test/ujs/views/layouts/application.html.erb
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html id="html">
+ <head>
+ <title><%= @title %></title>
+ <link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" />
+ <%= script_tag "http://code.jquery.com/jquery-2.2.0.js" %>
+ <script>
+ // This is for test in override.js.
+ // Must go before rails-ujs.
+ document.addEventListener('rails:attachBindings', function() {
+ window.Rails.linkClickSelector += ', a[data-custom-remote-link]';
+ // Hijacks link click before ujs binds any handlers
+ // This is only used for ctrl-clicking test on remote links
+ window.Rails.delegate(document, '#qunit-fixture a', 'click', function(e) {
+ e.preventDefault();
+ });
+ });
+ </script>
+ <%= script_tag "/rails-ujs.js" %>
+ </head>
+
+ <body id="body">
+ <%= yield %>
+ </body>
+</html>
diff --git a/actionview/test/ujs/views/tests/index.html.erb b/actionview/test/ujs/views/tests/index.html.erb
new file mode 100644
index 0000000000..8de6cd0695
--- /dev/null
+++ b/actionview/test/ujs/views/tests/index.html.erb
@@ -0,0 +1,11 @@
+<% @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' %>
+
+<h1 id="qunit-header"><%= @title %></h1>
+<h2 id="qunit-banner"></h2>
+<div id="qunit-testrunner-toolbar"></div>
+<h2 id="qunit-userAgent"></h2>
+<ol id="qunit-tests"></ol>
+
+<div id="qunit-fixture"></div>
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 5e8d8cb5c9..6b4f93df8b 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,41 +1 @@
-* Removed deprecated support to passing the adapter class to `.queue_adapter`.
-
- *Rafael Mendonça França*
-
-* Removed deprecated `#original_exception` in `ActiveJob::DeserializationError`.
-
- *Rafael Mendonça França*
-
-* Added instance variable `@queue` to JobWrapper.
-
- This will fix issues in [resque-scheduler](https://github.com/resque/resque-scheduler) `#job_to_hash` method,
- so we can use `#enqueue_delayed_selection`, `#remove_delayed` method in resque-scheduler smoothly.
-
- *mu29*
-
-* Yield the job instance so you have access to things like `job.arguments` on the custom logic after retries fail.
-
- *DHH*
-
-* Added declarative exception handling via `ActiveJob::Base.retry_on` and `ActiveJob::Base.discard_on`.
-
- Examples:
-
- class RemoteServiceJob < ActiveJob::Base
- retry_on CustomAppException # defaults to 3s wait, 5 attempts
- retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 }
- retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
- retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
- discard_on ActiveJob::DeserializationError
-
- def perform(*args)
- # Might raise CustomAppException or AnotherCustomAppException for something domain specific
- # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
- # Might raise Net::OpenTimeout when the remote service is down
- end
- end
-
- *DHH*
-
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activejob/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md) for previous changes.
diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE
index a3ffb46b3f..daa726b9f0 100644
--- a/activejob/MIT-LICENSE
+++ b/activejob/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014-2016 David Heinemeier Hansson
+Copyright (c) 2014-2017 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/Rakefile b/activejob/Rakefile
index 3953116061..41ff76135e 100644
--- a/activejob/Rakefile
+++ b/activejob/Rakefile
@@ -1,8 +1,9 @@
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 -= %w(queue_classic) if defined?(JRUBY_VERSION)
+#TODO: add delayed_job back to the list after it support Rails 5.1
+ACTIVEJOB_ADAPTERS = %w(async inline que queue_classic resque sidekiq sneakers sucker_punch backburner test)
+ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION)
task default: :test
task test: "test:default"
diff --git a/activejob/bin/test b/activejob/bin/test
new file mode 100755
index 0000000000..a7beb14b27
--- /dev/null
+++ b/activejob/bin/test
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require File.expand_path("../tools/test", COMPONENT_ROOT)
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
index 20ca7085c6..8b7aef65a2 100644
--- a/activejob/lib/active_job.rb
+++ b/activejob/lib/active_job.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2014-2016 David Heinemeier Hansson
+# Copyright (c) 2014-2017 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/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index 78eca1d2a1..c73117e7f3 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -18,8 +18,8 @@ module ActiveJob
job_or_instantiate(*args).enqueue
end
- protected
- def job_or_instantiate(*args)
+ private
+ def job_or_instantiate(*args) # :doc:
args.first.is_a?(self) ? args.first : new(*args)
end
end
diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb
index 0d50c27938..bf81f37e81 100644
--- a/activejob/lib/active_job/gem_version.rb
+++ b/activejob/lib/active_job/gem_version.rb
@@ -6,7 +6,7 @@ module ActiveJob
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
index aa97ab2e22..d7e2cd03e3 100644
--- a/activejob/lib/active_job/logging.rb
+++ b/activejob/lib/active_job/logging.rb
@@ -69,14 +69,14 @@ module ActiveJob
def perform_start(event)
info do
job = event.payload[:job]
- "Performing #{job.class.name} from #{queue_name(event)}" + args_info(job)
+ "Performing #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)}" + args_info(job)
end
end
def perform(event)
info do
job = event.payload[:job]
- "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2)}ms"
+ "Performed #{job.class.name} (Job ID: #{job.job_id}) from #{queue_name(event)} in #{event.duration.round(2)}ms"
end
end
diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb
index bcc555d33e..9dae80ffc2 100644
--- a/activejob/lib/active_job/queue_adapter.rb
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -1,4 +1,3 @@
-require "active_job/queue_adapters/inline_adapter"
require "active_support/core_ext/string/inflections"
module ActiveJob
diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb
index da042cfebf..1b633b210e 100644
--- a/activejob/lib/active_job/queue_adapters/test_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb
@@ -38,7 +38,6 @@ module ActiveJob
end
private
-
def job_to_hash(job, extras = {})
{ job: job.class, args: job.serialize.fetch("arguments"), queue: job.queue_name }.merge!(extras)
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index e6334d14ea..a61e4f59a5 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -8,16 +8,35 @@ module ActiveJob
:performed_jobs, :performed_jobs=,
to: :queue_adapter
+ module TestQueueAdapter
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false
+ end
+
+ module ClassMethods
+ def queue_adapter
+ self._test_adapter.nil? ? super : self._test_adapter
+ end
+
+ def disable_test_adapter
+ self._test_adapter = nil
+ end
+
+ def enable_test_adapter(test_adapter)
+ self._test_adapter = test_adapter
+ end
+ end
+ end
+
+ ActiveJob::Base.include(TestQueueAdapter)
+
def before_setup # :nodoc:
test_adapter = queue_adapter_for_test
- @old_queue_adapters = (ActiveJob::Base.descendants << ActiveJob::Base).select do |klass|
- # only override explicitly set adapters, a quirk of `class_attribute`
- klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
- end.map do |klass|
- [klass, klass.queue_adapter].tap do
- klass.queue_adapter = test_adapter
- end
+ queue_adapter_changed_jobs.each do |klass|
+ klass.enable_test_adapter(test_adapter)
end
clear_enqueued_jobs
@@ -27,9 +46,8 @@ module ActiveJob
def after_teardown # :nodoc:
super
- @old_queue_adapters.each do |(klass, adapter)|
- klass.queue_adapter = adapter
- end
+
+ queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter }
end
# Specifies the queue adapter to use with all active job test helpers.
@@ -55,7 +73,7 @@ module ActiveJob
# assert_enqueued_jobs 2
# end
#
- # If a block is passed, that block should cause the specified number of
+ # If a block is passed, that block will cause the specified number of
# jobs to be enqueued.
#
# def test_jobs_again
@@ -77,14 +95,23 @@ module ActiveJob
# HelloJob.perform_later('jeremy')
# end
# end
- def assert_enqueued_jobs(number, only: nil)
+ #
+ # The number of times a job is enqueued to a specific queue can also be asserted.
+ #
+ # def test_logging_job
+ # assert_enqueued_jobs 2, queue: 'default' do
+ # LoggingJob.perform_later
+ # HelloJob.perform_later('elfassy')
+ # end
+ # end
+ def assert_enqueued_jobs(number, only: nil, queue: nil)
if block_given?
- original_count = enqueued_jobs_size(only: only)
+ original_count = enqueued_jobs_size(only: only, queue: queue)
yield
- new_count = enqueued_jobs_size(only: only)
+ new_count = enqueued_jobs_size(only: only, queue: queue)
assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued"
else
- actual_count = enqueued_jobs_size(only: only)
+ actual_count = enqueued_jobs_size(only: only, queue: queue)
assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
end
end
@@ -284,7 +311,7 @@ module ActiveJob
# def test_perform_enqueued_jobs_with_only
# perform_enqueued_jobs(only: MyJob) do
# MyJob.perform_later(1, 2, 3) # will be performed
- # HelloJob.perform_later(1, 2, 3) # will not be perfomed
+ # HelloJob.perform_later(1, 2, 3) # will not be performed
# end
# assert_performed_jobs 1
# end
@@ -315,34 +342,46 @@ module ActiveJob
end
private
- def clear_enqueued_jobs # :nodoc:
+ def clear_enqueued_jobs
enqueued_jobs.clear
end
- def clear_performed_jobs # :nodoc:
+ def clear_performed_jobs
performed_jobs.clear
end
- def enqueued_jobs_size(only: nil) # :nodoc:
- if only
- enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) }
- else
- enqueued_jobs.count
+ def enqueued_jobs_size(only: nil, queue: nil)
+ enqueued_jobs.count do |job|
+ job_class = job.fetch(:job)
+ if only
+ next false unless Array(only).include?(job_class)
+ end
+ if queue
+ next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
+ end
+ true
end
end
- def serialize_args_for_assertion(args) # :nodoc:
+ def serialize_args_for_assertion(args)
args.dup.tap do |serialized_args|
serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args]
serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at]
end
end
- def instantiate_job(payload) # :nodoc:
+ def instantiate_job(payload)
job = payload[:job].new(*payload[:args])
job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
job.queue_name = payload[:queue]
job
end
+
+ def queue_adapter_changed_jobs
+ (ActiveJob::Base.descendants << ActiveJob::Base).select do |klass|
+ # only override explicitly set adapters, a quirk of `class_attribute`
+ klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
+ end
+ end
end
end
diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb
index 97c11a9ea6..50476a2e50 100644
--- a/activejob/lib/rails/generators/job/job_generator.rb
+++ b/activejob/lib/rails/generators/job/job_generator.rb
@@ -19,7 +19,7 @@ module Rails # :nodoc:
template "job.rb", File.join("app/jobs", class_path, "#{file_name}_job.rb")
in_root do
- if self.behavior == :invoke && !File.exist?(application_job_file_name)
+ if behavior == :invoke && !File.exist?(application_job_file_name)
template "application_job.rb", application_job_file_name
end
end
diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb
index 954974b2a5..b37736f859 100644
--- a/activejob/test/cases/logging_test.rb
+++ b/activejob/test/cases/logging_test.rb
@@ -89,21 +89,21 @@ class LoggingTest < ActiveSupport::TestCase
def test_perform_job_logging
LoggingJob.perform_later "Dummy"
- assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages)
+ assert_match(/Performing LoggingJob \(Job ID: .*?\) from .*? with arguments:.*Dummy/, @logger.messages)
assert_match(/Dummy, here is it: Dummy/, @logger.messages)
- assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages)
+ assert_match(/Performed LoggingJob \(Job ID: .*?\) from .*? in .*ms/, @logger.messages)
end
def test_perform_nested_jobs_logging
NestedJob.perform_later
assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages)
assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages)
- assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob \(Job ID: .*?\) from/, @logger.messages)
assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages)
- assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob \(Job ID: .*?\) from .* with arguments: "NestedJob"/, @logger.messages)
assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages)
- assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages)
- assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob \(Job ID: .*?\) from .* in/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob \(Job ID: .*?\) from .* in/, @logger.messages)
end
def test_enqueue_at_job_logging
diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb
index f1e0cf78ad..9611b0909b 100644
--- a/activejob/test/cases/queue_adapter_test.rb
+++ b/activejob/test/cases/queue_adapter_test.rb
@@ -21,6 +21,7 @@ class QueueAdapterTest < ActiveJob::TestCase
end
test "should allow overriding the queue_adapter at the child class level without affecting the parent or its sibling" do
+ ActiveJob::Base.disable_test_adapter
base_queue_adapter = ActiveJob::Base.queue_adapter
child_job_one = Class.new(ActiveJob::Base)
diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb
index 0e1e0decd1..0247bc111e 100644
--- a/activejob/test/cases/queue_naming_test.rb
+++ b/activejob/test/cases/queue_naming_test.rb
@@ -56,7 +56,7 @@ class QueueNamingTest < ActiveSupport::TestCase
original_queue_name = HelloJob.queue_name
begin
- HelloJob.queue_as { self.arguments.first == "1" ? :one : :two }
+ HelloJob.queue_as { arguments.first == "1" ? :one : :two }
assert_equal "one", HelloJob.new("1").queue_name
assert_equal "two", HelloJob.new("3").queue_name
ensure
diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb
index ec78a402d7..171fb1e593 100644
--- a/activejob/test/cases/queue_priority_test.rb
+++ b/activejob/test/cases/queue_priority_test.rb
@@ -3,7 +3,7 @@ require "jobs/hello_job"
class QueuePriorityTest < ActiveSupport::TestCase
test "priority unset by default" do
- assert_equal nil, HelloJob.priority
+ assert_nil HelloJob.priority
end
test "uses given priority" do
@@ -32,7 +32,7 @@ class QueuePriorityTest < ActiveSupport::TestCase
original_priority = HelloJob.priority
begin
- HelloJob.queue_with_priority { self.arguments.first == "1" ? 99 : 11 }
+ HelloJob.queue_with_priority { arguments.first == "1" ? 99 : 11 }
assert_equal 99, HelloJob.new("1").priority
assert_equal 11, HelloJob.new("3").priority
ensure
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
index 685c93da2d..81e75b4374 100644
--- a/activejob/test/cases/test_helper_test.rb
+++ b/activejob/test/cases/test_helper_test.rb
@@ -110,6 +110,27 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_enqueued_jobs_with_only_and_queue_option
+ assert_nothing_raised do
+ assert_enqueued_jobs 1, only: HelloJob, queue: :some_queue do
+ HelloJob.set(queue: :some_queue).perform_later
+ HelloJob.set(queue: :other_queue).perform_later
+ LoggingJob.perform_later
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_with_queue_option
+ assert_nothing_raised do
+ assert_enqueued_jobs 2, queue: :default do
+ HelloJob.perform_later
+ LoggingJob.perform_later
+ HelloJob.set(queue: :other_queue).perform_later
+ LoggingJob.set(queue: :other_queue).perform_later
+ end
+ end
+ end
+
def test_assert_enqueued_jobs_with_only_option_and_none_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_jobs 1, only: HelloJob do
@@ -256,11 +277,11 @@ end
class PerformedJobsTest < ActiveJob::TestCase
def test_performed_enqueue_jobs_with_only_option_doesnt_leak_outside_the_block
- assert_equal nil, queue_adapter.filter
+ assert_nil queue_adapter.filter
perform_enqueued_jobs only: HelloJob do
assert_equal HelloJob, queue_adapter.filter
end
- assert_equal nil, queue_adapter.filter
+ assert_nil queue_adapter.filter
end
def test_assert_performed_jobs
@@ -528,3 +549,20 @@ class InheritedJobTest < ActiveJob::TestCase
assert_instance_of ActiveJob::QueueAdapters::TestAdapter, InheritedJob.queue_adapter
end
end
+
+class QueueAdapterJobTest < ActiveJob::TestCase
+ def before_setup
+ @original_autoload_paths = ActiveSupport::Dependencies.autoload_paths
+ ActiveSupport::Dependencies.autoload_paths = %w(test/jobs)
+ super
+ end
+
+ def after_teardown
+ ActiveSupport::Dependencies.autoload_paths = @original_autoload_paths
+ super
+ end
+
+ def test_queue_adapter_is_test_adapter
+ assert_instance_of ActiveJob::QueueAdapters::TestAdapter, QueueAdapterJob.queue_adapter
+ end
+end
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
index 758506b3c0..db07ecab7a 100644
--- a/activejob/test/helper.rb
+++ b/activejob/test/helper.rb
@@ -1,10 +1,10 @@
require "active_job"
require "support/job_buffer"
-ActiveSupport.halt_callback_chains_on_return_false = false
GlobalID.app = "aj"
@adapter = ENV["AJ_ADAPTER"] || "inline"
+puts "Using #{@adapter}"
if ENV["AJ_INTEGRATION_TESTS"]
require "support/integration/helper"
diff --git a/activejob/test/jobs/application_job.rb b/activejob/test/jobs/application_job.rb
index 4a78b890ec..a009ace51c 100644
--- a/activejob/test/jobs/application_job.rb
+++ b/activejob/test/jobs/application_job.rb
@@ -1,4 +1,2 @@
-require_relative "../support/job_buffer"
-
class ApplicationJob < ActiveJob::Base
end
diff --git a/activejob/test/jobs/queue_adapter_job.rb b/activejob/test/jobs/queue_adapter_job.rb
new file mode 100644
index 0000000000..160dfd74ec
--- /dev/null
+++ b/activejob/test/jobs/queue_adapter_job.rb
@@ -0,0 +1,3 @@
+class QueueAdapterJob < ActiveJob::Base
+ self.queue_adapter = :inline
+end
diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb
index 263097c792..2e194933a1 100644
--- a/activejob/test/support/integration/adapters/backburner.rb
+++ b/activejob/test/support/integration/adapters/backburner.rb
@@ -23,12 +23,12 @@ module BackburnerJobsManager
end
def tube
- @tube ||= Beaneater::Tube.new(Backburner::Worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name
+ @tube ||= Beaneater::Tube.new(@worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name
end
def can_run?
begin
- Backburner::Worker.connection.send :connect!
+ @worker = Backburner::Worker.new
rescue
return false
end
diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb
index 911f70407e..1c8dfaca59 100644
--- a/activejob/test/support/integration/adapters/sneakers.rb
+++ b/activejob/test/support/integration/adapters/sneakers.rb
@@ -73,7 +73,7 @@ module SneakersJobsManager
true
end
- protected
+ private
def bunny_publisher
@bunny_publisher ||= begin
p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher)
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index 62f6fa13f6..29a5691f30 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -18,12 +18,13 @@ class TestJob < ActiveJob::Base
queue_as :integration_tests
def perform(x)
- File.open(Rails.root.join("tmp/\#{x}"), "wb+") do |f|
+ File.open(Rails.root.join("tmp/\#{x}.new"), "wb+") do |f|
f.write Marshal.dump({
"locale" => I18n.locale.to_s || "en",
"executed_at" => Time.now.to_r
})
end
+ File.rename(Rails.root.join("tmp/\#{x}.new"), Rails.root.join("tmp/\#{x}"))
end
end
CODE
diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb
index 1aaee2c809..626b932cce 100644
--- a/activejob/test/support/integration/helper.rb
+++ b/activejob/test/support/integration/helper.rb
@@ -5,6 +5,7 @@ ActiveJob::Base.queue_name_prefix = nil
require "rails/generators/rails/app/app_generator"
+require "tmpdir"
dummy_app_path = Dir.mktmpdir + "/dummy"
dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__)
args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle",
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index 3357489f20..41bf9c89d1 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -17,7 +17,7 @@ module TestCaseHelpers
end
end
- protected
+ private
def jobs_manager
JobsManager.current_manager
diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb
index 3cdc54e6d5..cf102ae5c2 100644
--- a/activejob/test/support/sneakers/inline.rb
+++ b/activejob/test/support/sneakers/inline.rb
@@ -4,7 +4,7 @@ module Sneakers
module Worker
module ClassMethods
def enqueue(msg)
- worker = self.new(nil, nil, {})
+ worker = new(nil, nil, {})
worker.work(*msg)
end
end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 10f1de6706..16b1b50cb3 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,12 +1,36 @@
-* Removed deprecated `:tokenizer` in the length validator.
+* Fix methods `#keys`, `#values` in `ActiveModel::Errors`.
- *Rafael Mendonça França*
+ Change `#keys` to only return the keys that don't have empty messages.
-* Removed deprecated methods in `ActiveModel::Errors`.
+ Change `#values` to only return the not empty values.
- `#get`, `#set`, `[]=`, `add_on_empty` and `add_on_blank`.
+ Example:
- *Rafael Mendonça França*
+ # 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 # => []
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md) for previous changes.
+ *bogdanvlviv*
+
+* Avoid converting integer as a string into float.
+
+ *namusyaka*
+
+
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index 8573eb1225..ac810e86d0 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2017 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/bin/test b/activemodel/bin/test
index 84a05bba08..a7beb14b27 100755
--- a/activemodel/bin/test
+++ b/activemodel/bin/test
@@ -2,5 +2,3 @@
COMPONENT_ROOT = File.expand_path("..", __dir__)
require File.expand_path("../tools/test", COMPONENT_ROOT)
-
-exit Minitest.run(ARGV)
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index a9b0663940..2389c858d5 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2017 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,7 +42,6 @@ module ActiveModel
autoload :Naming
autoload :SecurePassword
autoload :Serialization
- autoload :TestCase
autoload :Translation
autoload :Validations
autoload :Validator
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 1441b146f8..166c6ac21f 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -289,7 +289,7 @@ module ActiveModel
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
- send(generate_method, attr_name)
+ send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
end
@@ -334,12 +334,11 @@ module ActiveModel
}.tap { |mod| include mod }
end
- protected
- def instance_method_already_implemented?(method_name) #:nodoc:
+ private
+ def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
- private
# The methods +method_missing+ and +respond_to?+ of this module are
# invoked often in a typical rails, both of which invoke the method
# +matched_attribute_method+. The latter method iterates through an
@@ -349,11 +348,11 @@ module ActiveModel
# used to alleviate the GC, which ultimately also speeds up the app
# significantly (in our case our test suite finishes 10% faster with
# this cache).
- def attribute_method_matchers_cache #:nodoc:
+ def attribute_method_matchers_cache
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
- def attribute_method_matchers_matching(method_name) #:nodoc:
+ def attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
# will match every time.
@@ -365,7 +364,7 @@ module ActiveModel
# Define a method `name` in `mod` that dispatches to `send`
# using the given `extra` args. This falls back on `define_method`
# and `send` if the given names cannot be compiled.
- def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc:
+ def define_proxy_call(include_private, mod, name, send, *extra)
defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
@@ -458,12 +457,11 @@ module ActiveModel
end
end
- protected
- def attribute_method?(attr_name) #:nodoc:
+ private
+ def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
- private
# Returns a struct representing the matching attribute method.
# The struct's attributes are prefix, base and suffix.
def matched_attribute_method(method_name)
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 283090e380..eac2761433 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -103,7 +103,6 @@ module ActiveModel
def define_model_callbacks(*callbacks)
options = callbacks.extract_options!
options = {
- terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name],
only: [:before, :around, :after]
@@ -122,19 +121,19 @@ module ActiveModel
private
- def _define_before_model_callback(klass, callback) #:nodoc:
+ def _define_before_model_callback(klass, callback)
klass.define_singleton_method("before_#{callback}") do |*args, &block|
set_callback(:"#{callback}", :before, *args, &block)
end
end
- def _define_around_model_callback(klass, callback) #:nodoc:
+ def _define_around_model_callback(klass, callback)
klass.define_singleton_method("around_#{callback}") do |*args, &block|
set_callback(:"#{callback}", :around, *args, &block)
end
end
- def _define_after_model_callback(klass, callback) #:nodoc:
+ def _define_after_model_callback(klass, callback)
klass.define_singleton_method("after_#{callback}") do |*args, &block|
options = args.extract_options!
options[:prepend] = true
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 14adfa081f..5d3472802b 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -110,6 +110,7 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
+ attribute = attribute.to_sym
messages.key?(attribute) && messages[attribute].present?
end
alias :has_key? :include?
@@ -121,8 +122,9 @@ module ActiveModel
# person.errors.delete(:name) # => ["cannot be nil"]
# person.errors[:name] # => []
def delete(key)
- details.delete(key)
- messages.delete(key)
+ attribute = key.to_sym
+ details.delete(attribute)
+ messages.delete(attribute)
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -130,15 +132,6 @@ module ActiveModel
#
# person.errors[:name] # => ["cannot be nil"]
# person.errors['name'] # => ["cannot be nil"]
- #
- # Note that, if you try to get errors of an attribute which has
- # no errors associated with it, this method will instantiate
- # an empty error list for it and +keys+ will return an array
- # of error keys which includes this attribute.
- #
- # person.errors.keys # => []
- # person.errors[:name] # => []
- # person.errors.keys # => [:name]
def [](attribute)
messages[attribute.to_sym]
end
@@ -179,7 +172,9 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.values # => [["cannot be nil", "must be specified"]]
def values
- messages.values
+ messages.reject do |key, value|
+ value.empty?
+ end.values
end
# Returns all message keys.
@@ -187,7 +182,9 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
- messages.keys
+ messages.reject do |key, value|
+ value.empty?
+ end.keys
end
# Returns +true+ if no errors are found, +false+ otherwise.
@@ -342,6 +339,7 @@ module ActiveModel
# person.errors.full_messages_for(:name)
# # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
def full_messages_for(attribute)
+ attribute = attribute.to_sym
messages[attribute].map { |message| full_message(attribute, message) }
end
@@ -416,16 +414,23 @@ module ActiveModel
I18n.translate(key, options)
end
- def marshal_dump
+ def marshal_dump # :nodoc:
[@base, without_default_proc(@messages), without_default_proc(@details)]
end
- def marshal_load(array)
+ def marshal_load(array) # :nodoc:
@base, @messages, @details = array
apply_default_array(@messages)
apply_default_array(@details)
end
+ def init_with(coder) # :nodoc:
+ coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
+ @details ||= {}
+ apply_default_array(@messages)
+ apply_default_array(@details)
+ end
+
private
def normalize_message(attribute, message, options)
case message
diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb
index d2c6a89cc2..45ab8a2ca1 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -15,7 +15,7 @@ module ActiveModel
end
module ForbiddenAttributesProtection # :nodoc:
- protected
+ private
def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?)
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 4a8ee915cf..67bdfaa643 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -6,7 +6,7 @@ module ActiveModel
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index e683050787..945a5402a3 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -2,10 +2,10 @@ module ActiveModel
# == Active \Model \Basic \Model
#
# Includes the required interface for an object to interact with
- # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
+ # Action Pack and Action View, using different Active Model modules.
# It includes model name introspections, conversions, translations and
# validations. Besides that, it allows you to initialize the object with a
- # hash of attributes, pretty much like <tt>ActiveRecord</tt> does.
+ # hash of attributes, pretty much like Active Record does.
#
# A minimal implementation could be:
#
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 9532c63b65..9853cf38fe 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -234,7 +234,7 @@ module ActiveModel
# Person.model_name.plural # => "people"
def model_name
@_model_name ||= begin
- namespace = self.parents.detect do |n|
+ namespace = parents.detect do |n|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
end
ActiveModel::Name.new(self, namespace)
diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb
deleted file mode 100644
index 5004855d56..0000000000
--- a/activemodel/lib/active_model/test_case.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-module ActiveModel #:nodoc:
- class TestCase < ActiveSupport::TestCase #:nodoc:
- end
-end
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index b8cf43cc10..35fc7cf743 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -44,7 +44,7 @@ module ActiveModel
parts = attribute.to_s.split(".")
attribute = parts.pop
namespace = parts.join("/") unless parts.empty?
- attributes_scope = "#{self.i18n_scope}.attributes"
+ attributes_scope = "#{i18n_scope}.attributes"
if namespace
defaults = lookup_ancestors.map do |klass|
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
index 0d3349e236..095801d8f0 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -7,14 +7,11 @@ 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/decimal_without_scale"
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/text"
require "active_model/type/time"
-require "active_model/type/unsigned_integer"
require "active_model/type/registry"
@@ -24,16 +21,8 @@ module ActiveModel
class << self
attr_accessor :registry # :nodoc:
- delegate :add_modifier, to: :registry
- # Add a new type to the registry, allowing it to be referenced as a
- # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your
- # type is only meant to be used with a specific database adapter, you can
- # do so by passing +adapter: :postgresql+. If your type has the same
- # name as a native type for the current adapter, an exception will be
- # raised unless you specify an +:override+ option. +override: true+ will
- # cause your type to be used instead of the native type. +override:
- # false+ will cause the native type to be used over yours if one exists.
+ # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup
def register(type_name, klass = nil, **options, &block)
registry.register(type_name, klass, **options, &block)
end
@@ -53,7 +42,6 @@ module ActiveModel
register(:immutable_string, Type::ImmutableString)
register(:integer, Type::Integer)
register(:string, Type::String)
- register(:text, Type::Text)
register(:time, Type::Time)
end
end
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 6e313fbca8..eefd080351 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -12,7 +12,7 @@ module ActiveModel
end
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ value.to_s(:db).inspect
end
private
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index 6c5c0451c6..e6805c5f6b 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -4,6 +4,7 @@ module ActiveModel
module Type
class Decimal < Value # :nodoc:
include Helpers::Numeric
+ BIGDECIMAL_PRECISION = 18
def type
:decimal
@@ -20,8 +21,14 @@ module ActiveModel
case value
when ::Float
convert_float_to_big_decimal(value)
- when ::Numeric, ::String
- BigDecimal(value, precision.to_i)
+ when ::Numeric
+ BigDecimal(value, precision || BIGDECIMAL_PRECISION)
+ when ::String
+ begin
+ value.to_d
+ rescue ArgumentError
+ BigDecimal(0)
+ end
else
if value.respond_to?(:to_d)
value.to_d
diff --git a/activemodel/lib/active_model/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb
deleted file mode 100644
index 985e1038ed..0000000000
--- a/activemodel/lib/active_model/type/decimal_without_scale.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "active_model/type/big_integer"
-
-module ActiveModel
- module Type
- class DecimalWithoutScale < BigInteger # :nodoc:
- def type
- :decimal
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
index facea12704..f783d286c5 100644
--- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -1,7 +1,7 @@
module ActiveModel
module Type
- module Helpers
- class AcceptsMultiparameterTime < Module # :nodoc:
+ module Helpers # :nodoc: all
+ class AcceptsMultiparameterTime < Module
def initialize(defaults: {})
define_method(:cast) do |value|
if value.is_a?(Hash)
diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb
index 4dddbe4e5e..f3a17a1698 100644
--- a/activemodel/lib/active_model/type/helpers/mutable.rb
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -1,7 +1,7 @@
module ActiveModel
module Type
- module Helpers
- module Mutable # :nodoc:
+ module Helpers # :nodoc: all
+ module Mutable
def cast(value)
deserialize(serialize(value))
end
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index 98533f8771..275822b738 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -1,7 +1,7 @@
module ActiveModel
module Type
- module Helpers
- module Numeric # :nodoc:
+ module Helpers # :nodoc: all
+ module Numeric
def cast(value)
value = \
case value
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
index ad78fd49ec..53cf7c6029 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -2,8 +2,8 @@ require "active_support/core_ext/time/zones"
module ActiveModel
module Type
- module Helpers
- module TimeValue # :nodoc:
+ module Helpers # :nodoc: all
+ module TimeValue
def serialize(value)
value = apply_seconds_precision(value)
@@ -34,11 +34,11 @@ module ActiveModel
return value unless precision && value.respond_to?(:usec)
number_of_insignificant_digits = 6 - precision
round_power = 10**number_of_insignificant_digits
- value.change(usec: value.usec / round_power * round_power)
+ value.change(usec: value.usec - value.usec % round_power)
end
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ value.to_s(:db).inspect
end
def user_input_in_time_zone(value)
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index 41dd655a5c..106b5d966c 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -29,6 +29,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
@@ -46,7 +48,7 @@ module ActiveModel
def ensure_in_range(value)
unless range.cover?(value)
- raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
end
end
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
index d25f1a129e..2d5dd366eb 100644
--- a/activemodel/lib/active_model/type/registry.rb
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -21,6 +21,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
@@ -55,6 +57,8 @@ 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
attr_reader :name, :block
diff --git a/activemodel/lib/active_model/type/text.rb b/activemodel/lib/active_model/type/text.rb
deleted file mode 100644
index 7c0d647706..0000000000
--- a/activemodel/lib/active_model/type/text.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "active_model/type/string"
-
-module ActiveModel
- module Type
- class Text < String # :nodoc:
- def type
- :text
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index a1f7c971db..d460068830 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -399,14 +399,14 @@ module ActiveModel
# end
alias :read_attribute_for_validation :send
- protected
+ private
- def run_validations! #:nodoc:
+ def run_validations!
_run_validate_callbacks
errors.empty?
end
- def raise_validation_error
+ def raise_validation_error # :doc:
raise(ValidationError.new(self))
end
end
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
index 75bf655578..4618f46e30 100644
--- a/activemodel/lib/active_model/validations/absence.rb
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -22,7 +22,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_absence_of(*attr_names)
validates_with AbsenceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 9826c2fe9c..a26c37daa5 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -56,6 +56,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
@@ -93,7 +95,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information.
+ # See <tt>ActiveModel::Validations#validates</tt> for more information.
def validates_acceptance_of(*attr_names)
validates_with AcceptanceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index a201f72ed0..4e94422cf1 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -23,7 +23,6 @@ module ActiveModel
included do
include ActiveSupport::Callbacks
define_callbacks :validation,
- terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name]
end
@@ -104,10 +103,10 @@ module ActiveModel
end
end
- protected
+ private
# Overwrite run validations to include callbacks.
- def run_validations! #:nodoc:
+ def run_validations!
_run_validation_callbacks { super }
end
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 33ca6f6946..03585bf5e1 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -69,7 +69,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_confirmation_of(*attr_names)
validates_with ConfirmationValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index 82a1893823..b7156ba802 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -38,7 +38,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_exclusion_of(*attr_names)
validates_with ExclusionValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index fa183885ab..b4b8d9f33c 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -104,7 +104,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_format_of(*attr_names)
validates_with FormatValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 0ea6012a5d..c6c5bae649 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -36,7 +36,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_inclusion_of(*attr_names)
validates_with InclusionValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index de1524829e..940c58f3a7 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,5 +1,3 @@
-require "active_support/core_ext/string/strip"
-
module ActiveModel
module Validations
class LengthValidator < EachValidator # :nodoc:
@@ -112,7 +110,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+ and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_length_of(*attr_names)
validates_with LengthValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 48e0e5c9f6..995b331245 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -61,7 +61,7 @@ module ActiveModel
end
end
- protected
+ private
def is_number?(raw_value)
!parse_raw_value_as_a_number(raw_value).nil?
@@ -70,6 +70,7 @@ module ActiveModel
end
def parse_raw_value_as_a_number(raw_value)
+ return raw_value.to_i if is_integer?(raw_value)
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
end
@@ -94,12 +95,10 @@ module ActiveModel
end
end
- private
-
- def record_attribute_changed_in_place?(record, attr_name)
- record.respond_to?(:attribute_changed_in_place?) &&
- record.attribute_changed_in_place?(attr_name.to_s)
- end
+ def record_attribute_changed_in_place?(record, attr_name)
+ record.respond_to?(:attribute_changed_in_place?) &&
+ record.attribute_changed_in_place?(attr_name.to_s)
+ end
end
module HelperMethods
@@ -136,7 +135,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
#
# The following checks can also be supplied with a proc or a symbol which
# corresponds to a method:
diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb
index 0c11cf4265..6e8a434bbe 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -29,7 +29,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index f95f44de61..0ce5935f3a 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -148,15 +148,15 @@ module ActiveModel
validates(*(attributes << options))
end
- protected
+ private
# 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 # :nodoc:
+ def _validates_default_keys
[:if, :unless, :on, :allow_blank, :allow_nil , :strict]
end
- def _parse_validates_options(options) # :nodoc:
+ def _parse_validates_options(options)
case options
when TrueClass
{}
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index e3f7a9bcb2..9227dd06ff 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -61,7 +61,7 @@ module ActiveModel
# The method, proc or string should return or evaluate to a +true+ or
# +false+ value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
- # See <tt>ActiveModel::Validation#validates!</tt> for more information.
+ # See <tt>ActiveModel::Validations#validates!</tt> for more information.
#
# If you pass any additional configuration options, they will be passed
# to the class and available as +options+:
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 8212744170..98234e9b6b 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -141,8 +141,8 @@ module ActiveModel
end
# Performs validation on the supplied record. By default this will call
- # +validates_each+ to determine validity therefore subclasses should
- # override +validates_each+ with validation logic.
+ # +validate_each+ to determine validity therefore subclasses should
+ # override +validate_each+ with validation logic.
def validate(record)
attributes.each do |attribute|
value = record.read_attribute_for_validation(attribute)
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index 8eb389d331..fa41d1b95f 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -16,6 +16,8 @@ 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
attr_writer :metadata
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
index 63b6c56f8c..f85cd7dec4 100644
--- a/activemodel/test/cases/callbacks_test.rb
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -27,7 +27,7 @@ class CallbacksTest < ActiveModel::TestCase
false
end
- ActiveSupport::Deprecation.silence { after_create "@callbacks << :final_callback" }
+ after_create { |model| model.callbacks << :final_callback }
def initialize(options = {})
@callbacks = []
@@ -63,12 +63,10 @@ class CallbacksTest < ActiveModel::TestCase
assert_equal model.callbacks.last, :final_callback
end
- test "the callback chain is halted when a before callback returns false (deprecated)" do
+ test "the callback chain is not halted when a before callback returns false)" do
model = ModelCallbacks.new(before_create_returns: false)
- assert_deprecated do
- model.create
- assert_equal model.callbacks.last, :before_create
- end
+ model.create
+ assert_equal model.callbacks.last, :final_callback
end
test "the callback chain is halted when a callback throws :abort" do
@@ -127,6 +125,7 @@ class CallbacksTest < ActiveModel::TestCase
test "after_create callbacks with both callbacks declared in one line" do
assert_equal ["callback1", "callback2"], Violin1.new.create.history
end
+
test "after_create callbacks with both callbacks declared in different lines" do
assert_equal ["callback1", "callback2"], Violin2.new.create.history
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 605ad64e4d..43aee5a814 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -1,4 +1,6 @@
require "cases/helper"
+require "active_support/core_ext/string/strip"
+require "yaml"
class ErrorsTest < ActiveModel::TestCase
class Person
@@ -30,7 +32,7 @@ class ErrorsTest < ActiveModel::TestCase
def test_delete
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
- errors.delete(:foo)
+ errors.delete("foo")
assert_empty errors[:foo]
end
@@ -38,6 +40,7 @@ class ErrorsTest < ActiveModel::TestCase
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
assert_includes errors, :foo, "errors should include :foo"
+ assert_includes errors, "foo", "errors should include 'foo' as :foo"
end
def test_dup
@@ -52,6 +55,7 @@ class ErrorsTest < ActiveModel::TestCase
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
assert_equal true, errors.has_key?(:foo), "errors should have key :foo"
+ assert_equal true, errors.has_key?("foo"), "errors should have key 'foo' as :foo"
end
def test_has_no_key
@@ -63,6 +67,7 @@ class ErrorsTest < ActiveModel::TestCase
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
assert_equal true, errors.key?(:foo), "errors should have key :foo"
+ assert_equal true, errors.key?("foo"), "errors should have key 'foo' as :foo"
end
def test_no_key
@@ -94,6 +99,14 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["omg", "zomg"], errors.values
end
+ test "values returns an empty array after try to get a message only" do
+ errors = ActiveModel::Errors.new(self)
+ errors.messages[:foo]
+ errors.messages[:baz]
+
+ assert_equal [], errors.values
+ end
+
test "keys returns the error keys" do
errors = ActiveModel::Errors.new(self)
errors.messages[:foo] << "omg"
@@ -102,6 +115,14 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal [:foo, :baz], errors.keys
end
+ test "keys returns an empty array after try to get a message only" do
+ errors = ActiveModel::Errors.new(self)
+ errors.messages[:foo]
+ errors.messages[:baz]
+
+ assert_equal [], errors.keys
+ end
+
test "detecting whether there are errors with empty?, blank?, include?" do
person = Person.new
person.errors[:foo]
@@ -150,10 +171,11 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["cannot be blank"], person.errors[:name]
end
- test "added? detects if a specific error was added to the object" do
+ test "added? detects indifferent if a specific error was added to the object" do
person = Person.new
person.errors.add(:name, "cannot be blank")
assert person.errors.added?(:name, "cannot be blank")
+ assert person.errors.added?("name", "cannot be blank")
end
test "added? handles symbol message" do
@@ -241,7 +263,7 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages
end
- test "full_messages_for contains all the error messages for the given attribute" do
+ test "full_messages_for contains all the error messages for the given attribute indifferent" do
person = Person.new
person.errors.add(:name, "cannot be blank")
person.errors.add(:name, "cannot be nil")
@@ -253,6 +275,7 @@ class ErrorsTest < ActiveModel::TestCase
person.errors.add(:name, "cannot be blank")
person.errors.add(:email, "cannot be blank")
assert_equal ["name cannot be blank"], person.errors.full_messages_for(:name)
+ assert_equal ["name cannot be blank"], person.errors.full_messages_for("name")
end
test "full_messages_for returns an empty list in case there are no errors for the given attribute" do
@@ -360,4 +383,24 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal errors.messages, serialized.messages
assert_equal errors.details, serialized.details
end
+
+ test "errors are backward compatible with the Rails 4.2 format" do
+ yaml = <<-CODE.strip_heredoc
+ --- !ruby/object:ActiveModel::Errors
+ base: &1 !ruby/object:ErrorsTest::Person
+ errors: !ruby/object:ActiveModel::Errors
+ base: *1
+ messages: {}
+ messages: {}
+ CODE
+
+ errors = YAML.load(yaml)
+ errors.add(:name, :invalid)
+ assert_equal({ name: ["is invalid"] }, errors.messages)
+ assert_equal({ name: [{ error: :invalid }] }, errors.details)
+
+ errors.clear
+ assert_equal({}, errors.messages)
+ assert_equal({}, errors.details)
+ end
end
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 4e9f43ad86..eeb5c85a48 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -9,15 +9,15 @@ I18n.enforce_available_locales = false
require "active_support/testing/autorun"
require "active_support/testing/method_call_assertions"
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
-end
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
-end
-
-class ActiveModel::TestCase
+class ActiveModel::TestCase < ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ private def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ private def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 9423df2c09..77ce86f392 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -179,7 +179,7 @@ class SecurePasswordTest < ActiveModel::TestCase
test "setting a nil password should clear an existing password" do
@existing_user.password = nil
- assert_equal nil, @existing_user.password_digest
+ assert_nil @existing_user.password_digest
end
test "authenticate" do
diff --git a/activemodel/test/cases/type/binary_test.rb b/activemodel/test/cases/type/binary_test.rb
index e6a32dbeec..e9c2ccfca4 100644
--- a/activemodel/test/cases/type/binary_test.rb
+++ b/activemodel/test/cases/type/binary_test.rb
@@ -6,7 +6,7 @@ module ActiveModel
class BinaryTest < ActiveModel::TestCase
def test_type_cast_binary
type = Type::Binary.new
- assert_equal nil, type.cast(nil)
+ assert_nil type.cast(nil)
assert_equal "1", type.cast("1")
assert_equal 1, type.cast(1)
end
diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb
index 44e20a327b..0cc90e99d3 100644
--- a/activemodel/test/cases/type/date_test.rb
+++ b/activemodel/test/cases/type/date_test.rb
@@ -6,10 +6,10 @@ module ActiveModel
class DateTest < ActiveModel::TestCase
def test_type_cast_date
type = Type::Date.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast("")
- assert_equal nil, type.cast(" ")
- assert_equal nil, type.cast("ABC")
+ assert_nil type.cast(nil)
+ assert_nil type.cast("")
+ assert_nil type.cast(" ")
+ assert_nil type.cast("ABC")
date_string = ::Time.now.utc.strftime("%F")
assert_equal date_string, type.cast(date_string).strftime("%F")
diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb
index fb82260d2b..75a7fc686e 100644
--- a/activemodel/test/cases/type/date_time_test.rb
+++ b/activemodel/test/cases/type/date_time_test.rb
@@ -6,10 +6,10 @@ module ActiveModel
class DateTimeTest < ActiveModel::TestCase
def test_type_cast_datetime_and_timestamp
type = Type::DateTime.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast("")
- assert_equal nil, type.cast(" ")
- assert_equal nil, type.cast("ABC")
+ assert_nil type.cast(nil)
+ assert_nil type.cast("")
+ assert_nil type.cast(" ")
+ assert_nil type.cast("ABC")
datetime_string = ::Time.now.utc.strftime("%FT%T")
assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T")
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index 46a913258e..c3b43725cc 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -11,6 +11,14 @@ module ActiveModel
assert_equal BigDecimal.new("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")
+ 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)
diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb
index 2e34f57f7e..8026d63ad5 100644
--- a/activemodel/test/cases/type/float_test.rb
+++ b/activemodel/test/cases/type/float_test.rb
@@ -9,6 +9,14 @@ module ActiveModel
assert_equal 1.0, type.cast("1")
end
+ def test_type_cast_float_from_invalid_string
+ type = Type::Float.new
+ assert_nil type.cast("")
+ assert_equal 1.0, type.cast("1ignore")
+ assert_equal 0.0, type.cast("bad1")
+ assert_equal 0.0, type.cast("bad")
+ end
+
def test_changing_float
type = Type::Float.new
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
index 2b9b03f3cf..a91144036b 100644
--- a/activemodel/test/cases/type/integer_test.rb
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -7,6 +7,7 @@ module ActiveModel
class IntegerTest < ActiveModel::TestCase
test "simple values" do
type = Type::Integer.new
+ assert_nil type.cast("")
assert_equal 1, type.cast(1)
assert_equal 1, type.cast("1")
assert_equal 1, type.cast("1ignore")
diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb
index a6a79833e6..0cc4d33caa 100644
--- a/activemodel/test/cases/type/time_test.rb
+++ b/activemodel/test/cases/type/time_test.rb
@@ -6,9 +6,9 @@ module ActiveModel
class TimeTest < ActiveModel::TestCase
def test_type_cast_time
type = Type::Time.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast("")
- assert_equal nil, type.cast("ABC")
+ assert_nil type.cast(nil)
+ assert_nil type.cast("")
+ assert_nil type.cast("ABC")
time_string = ::Time.now.utc.strftime("%T")
assert_equal time_string, type.cast(time_string).strftime("%T")
diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb
index 83e8ac9522..f2e4a5946d 100644
--- a/activemodel/test/cases/validations/callbacks_test.rb
+++ b/activemodel/test/cases/validations/callbacks_test.rb
@@ -29,7 +29,7 @@ class DogWithTwoValidators < Dog
before_validation { history << "before_validation_marker2" }
end
-class DogDeprecatedBeforeValidatorReturningFalse < Dog
+class DogBeforeValidatorReturningFalse < Dog
before_validation { false }
before_validation { history << "before_validation_marker2" }
end
@@ -121,13 +121,11 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
assert_equal false, output
end
- def test_deprecated_further_callbacks_should_not_be_called_if_before_validation_returns_false
- d = DogDeprecatedBeforeValidatorReturningFalse.new
- assert_deprecated do
- output = d.valid?
- assert_equal [], d.history
- assert_equal false, output
- end
+ def test_further_callbacks_should_be_called_if_before_validation_returns_false
+ d = DogBeforeValidatorReturningFalse.new
+ output = d.valid?
+ assert_equal ["before_validation_marker2"], d.history
+ assert_equal true, output
end
def test_further_callbacks_should_be_called_if_after_validation_returns_false
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index 4881008017..048d27446e 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -43,7 +43,9 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_if_validation_using_string_true
# When the evaluated string returns true
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
+ ActiveSupport::Deprecation.silence do
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
+ end
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -52,7 +54,9 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_unless_validation_using_string_true
# When the evaluated string returns true
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
+ ActiveSupport::Deprecation.silence do
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
+ end
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert_empty t.errors[:title]
@@ -60,7 +64,9 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_if_validation_using_string_false
# When the evaluated string returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
+ ActiveSupport::Deprecation.silence do
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
+ end
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert_empty t.errors[:title]
@@ -68,7 +74,9 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_unless_validation_using_string_false
# When the evaluated string returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
+ ActiveSupport::Deprecation.silence do
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
+ end
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -118,7 +126,9 @@ class ConditionalValidationTest < ActiveModel::TestCase
# ensure that it works correctly
def test_validation_with_if_as_string
Topic.validates_presence_of(:title)
- Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')")
+ 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"
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index a1be2de578..c0158e075f 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -260,6 +260,15 @@ class NumericalityValidationTest < ActiveModel::TestCase
Person.clear_validators!
end
+ def test_validates_numericality_with_exponent_number
+ base = 10_000_000_000_000_000
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: base
+ topic = Topic.new
+ topic.approved = (base + 1).to_s
+
+ assert topic.invalid?
+ end
+
def test_validates_numericality_with_invalid_args
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" }
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" }
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 20c11dd852..5ce86738cd 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -69,26 +69,34 @@ class ValidatesWithTest < ActiveModel::TestCase
end
test "with if statements that return false" do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2")
+ 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
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
+ 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
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1")
+ 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
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
+ 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
@@ -102,7 +110,9 @@ class ValidatesWithTest < ActiveModel::TestCase
validator.expect(:is_a?, false, [Symbol])
validator.expect(:is_a?, false, [String])
- Topic.validates_with(validator, if: "1 == 1", foo: :bar)
+ ActiveSupport::Deprecation.silence do
+ Topic.validates_with(validator, if: "1 == 1", foo: :bar)
+ end
assert topic.valid?
validator.verify
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index a8bed82e19..30d580b9e3 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,305 +1 @@
-* Allow `ActionController::Parameters`-like objects to be passed as
- values for Postgres HStore columns.
-
- Fixes #26904.
-
- *Jon Moss*
-
-* Added `stat` method to `ActiveRecord::ConnectionAdapters::ConnectionPool`
-
- Example:
-
- ActiveRecord::Base.connection_pool.stat # =>
- { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }
-
- *Pavel Evstigneev*
-
-* Avoid `unscope(:order)` when `limit_value` is presented for `count`
- and `exists?`.
-
- If `limit_value` is presented, records fetching order is very important
- for performance. We should not unscope the order in the case.
-
- *Ryuta Kamizono*
-
-* Fix an Active Record `DateTime` field `NoMethodError` caused by incomplete
- datetime.
-
- Fixes #24195.
-
- *Sen Zhang*
-
-* Allow `slice` to take an array of methods(without the need for splatting).
-
- *Cohen Carlisle*
-
-* Improved partial writes with HABTM and has many through associations
- to fire database query only if relation has been changed.
-
- Fixes #19663.
-
- *Mehmet Emin İNAÇ*
-
-* Deprecate passing arguments and block at the same time to
- `ActiveRecord::QueryMethods#select`.
-
- *Prathamesh Sonpatki*
-
-* Optimistic locking: Added ability to update `locking_column` value.
- Ignore optimistic locking if trying to update with new `locking_column` value.
-
- *bogdanvlviv*
-
-* Fixed: Optimistic locking does not work well with `null` in the database.
-
- Fixes #26024
-
- *bogdanvlviv*
-
-* Fixed support for case insensitive comparisons of `text` columns in
- PostgreSQL.
-
- *Edho Arief*
-
-* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null`
-
- *Trung Duc Tran*
-
-* Return `true` from `update_attribute` when the value of the attribute
- to be updated is unchanged.
-
- Fixes #26593.
-
- *Prathamesh Sonpatki*
-
-* Always store errors details information with symbols.
-
- When the association is autosaved we were storing the details with
- string keys. This was creating inconsistency with other details that are
- added using the `Errors#add` method. It was also inconsistent with the
- `Errors#messages` storage.
-
- To fix this inconsistency we are always storing with symbols. This will
- cause a small breaking change because in those cases the details could
- be accessed as strings keys but now it can not.
-
- Fix #26499.
-
- *Rafael Mendonça França*, *Marcus Vieira*
-
-* Calling `touch` on a model using optimistic locking will now leave the model
- in a non-dirty state with no attribute changes.
-
- Fixes #26496.
-
- *Jakob Skjerning*
-
-* Using a mysql2 connection after it fails to reconnect will now have an error message
- saying the connection is closed rather than an undefined method error message.
-
- *Dylan Thacker-Smith*
-
-* PostgreSQL array columns will now respect the encoding of strings contained
- in the array.
-
- Fixes #26326.
-
- *Sean Griffin*
-
-* Inverse association instances will now be set before `after_find` or
- `after_initialize` callbacks are run.
-
- Fixes #26320.
-
- *Sean Griffin*
-
-* Remove unnecessarily association load when a `belongs_to` association has already been
- loaded then the foreign key is changed directly and the record saved.
-
- *James Coleman*
-
-* Remove standardized column types/arguments spaces in schema dump.
-
- *Tim Petricola*
-
-* Avoid loading records from database when they are already loaded using
- the `pluck` method on a collection.
-
- Fixes #25921.
-
- *Ryuta Kamizono*
-
-* Remove text default treated as an empty string in non-strict mode for
- consistency with other types.
-
- Strict mode controls how MySQL handles invalid or missing values in
- data-change statements such as INSERT or UPDATE. If strict mode is not
- in effect, MySQL inserts adjusted values for invalid or missing values
- and produces warnings.
-
- def test_mysql_not_null_defaults_non_strict
- using_strict(false) do
- with_mysql_not_null_table do |klass|
- record = klass.new
- assert_nil record.non_null_integer
- assert_nil record.non_null_string
- assert_nil record.non_null_text
- assert_nil record.non_null_blob
-
- record.save!
- record.reload
-
- assert_equal 0, record.non_null_integer
- assert_equal "", record.non_null_string
- assert_equal "", record.non_null_text
- assert_equal "", record.non_null_blob
- end
- end
- end
-
- https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict
-
- *Ryuta Kamizono*
-
-* Sqlite3 migrations to add a column to an existing table can now be
- successfully rolled back when the column was given and invalid column
- type.
-
- Fixes #26087
-
- *Travis O'Neill*
-
-* Deprecate `sanitize_conditions`. Use `sanitize_sql` instead.
-
- *Ryuta Kamizono*
-
-* Doing count on relations that contain LEFT OUTER JOIN Arel node no longer
- force a DISTINCT. This solves issues when using count after a left_joins.
-
- *Maxime Handfield Lapointe*
-
-* RecordNotFound raised by association.find exposes `id`, `primary_key` and
- `model` methods to be consistent with RecordNotFound raised by Record.find.
-
- *Michel Pigassou*
-
-* Hashes can once again be passed to setters of `composed_of`, if all of the
- mapping methods are methods implemented on `Hash`.
-
- Fixes #25978.
-
- *Sean Griffin*
-
-* Fix the SELECT statement in `#table_comment` for MySQL.
-
- *Takeshi Akima*
-
-* Virtual attributes will no longer raise when read on models loaded from the
- database
-
- *Sean Griffin*
-
-* Support calling the method `merge` in `scope`'s lambda.
-
- *Yasuhiro Sugino*
-
-* Fixes multi-parameter attributes conversion with invalid params.
-
- *Hiroyuki Ishii*
-
-* Add newline between each migration in `structure.sql`.
-
- Keeps schema migration inserts as a single commit, but allows for easier
- git diffing.
-
- Fixes #25504.
-
- *Grey Baker*, *Norberto Lopes*
-
-* The flag `error_on_ignored_order_or_limit` has been deprecated in favor of
- the current `error_on_ignored_order`.
-
- *Xavier Noria*
-
-* Batch processing methods support `limit`:
-
- Post.limit(10_000).find_each do |post|
- # ...
- end
-
- It also works in `find_in_batches` and `in_batches`.
-
- *Xavier Noria*
-
-* Using `group` with an attribute that has a custom type will properly cast
- the hash keys after calling a calculation method like `count`.
-
- Fixes #25595.
-
- *Sean Griffin*
-
-* Fix the generated `#to_param` method to use `omission: ''` so that
- the resulting output is actually up to 20 characters, not
- effectively 17 to leave room for the default "...".
- Also call `#parameterize` before `#truncate` and make the
- `separator: /-/` to maximize the information included in the
- output.
-
- Fixes #23635.
-
- *Rob Biedenharn*
-
-* Ensure concurrent invocations of the connection reaper cannot allocate the
- same connection to two threads.
-
- Fixes #25585.
-
- *Matthew Draper*
-
-* Inspecting an object with an associated array of over 10 elements no longer
- truncates the array, preventing `inspect` from looping infinitely in some
- cases.
-
- *Kevin McPhillips*
-
-* Removed the unused methods `ActiveRecord::Base.connection_id` and
- `ActiveRecord::Base.connection_id=`.
-
- *Sean Griffin*
-
-* Ensure hashes can be assigned to attributes created using `composed_of`.
-
- Fixes #25210.
-
- *Sean Griffin*
-
-* Fix logging edge case where if an attribute was of the binary type and
- was provided as a Hash.
-
- *Jon Moss*
-
-* Handle JSON deserialization correctly if the column default from database
- adapter returns `''` instead of `nil`.
-
- *Johannes Opper*
-
-* Introduce `ActiveRecord::TransactionSerializationError` for catching
- transaction serialization failures or deadlocks.
-
- *Erol Fornoles*
-
-* PostgreSQL: Fix `db:structure:load` silent failure on SQL error.
-
- The command line flag `-v ON_ERROR_STOP=1` should be used
- when invoking `psql` to make sure errors are not suppressed.
-
- Example:
-
- psql -v ON_ERROR_STOP=1 -q -f awesome-file.sql my-app-db
-
- Fixes #23818.
-
- *Ralin Chimev*
-
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 1f496cf280..f9e4444f07 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2017 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/activerecord/Rakefile b/activerecord/Rakefile
index ec28df8fea..7be3d851f1 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -111,11 +111,6 @@ namespace :db do
config = ARTest.config["connections"]["postgresql"]
%x( createdb -E UTF8 -T template0 #{config["arunit"]["database"]} )
%x( createdb -E UTF8 -T template0 #{config["arunit2"]["database"]} )
-
- # prepare hstore
- if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
- puts "Please prepare hstore data type. See http://www.postgresql.org/docs/current/static/hstore.html"
- end
end
desc "Drop the PostgreSQL test databases"
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 2cd8f179dd..0b37e9076c 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -24,5 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency "activesupport", version
s.add_dependency "activemodel", version
- s.add_dependency "arel", "~> 7.0"
+ s.add_dependency "arel", "~> 8.0"
end
diff --git a/activerecord/bin/test b/activerecord/bin/test
index 23add35d45..3a9547e5c1 100755
--- a/activerecord/bin/test
+++ b/activerecord/bin/test
@@ -17,5 +17,3 @@ module Minitest
end
Minitest.extensions.unshift "active_record"
-
-exit Minitest.run(ARGV)
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 3b5dab16cb..96b8545dfc 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2017 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -44,7 +44,6 @@ module ActiveRecord
autoload :Explain
autoload :Inheritance
autoload :Integration
- autoload :LegacyYamlAdapter
autoload :Migration
autoload :Migrator, "active_record/migration"
autoload :ModelSchema
@@ -85,6 +84,8 @@ module ActiveRecord
autoload :AttributeMethods
autoload :AutosaveAssociation
+ autoload :LegacyYamlAdapter
+
autoload :Relation
autoload :AssociationRelation
autoload :NullRelation
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 08dfc3a64f..10cbd5429c 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -15,11 +15,11 @@ module ActiveRecord
private
- def clear_aggregation_cache # :nodoc:
+ def clear_aggregation_cache
@aggregation_cache.clear if persisted?
end
- def init_internals # :nodoc:
+ def init_internals
@aggregation_cache = {}
super
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 3c94c4bd7f..6efa448d49 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -97,6 +97,16 @@ module ActiveRecord
end
end
+ class HasManyThroughOrderError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
+ if owner_class_name && reflection && through_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.")
+ else
+ super("Cannot have a has_many :through association before the through association is defined.")
+ end
+ end
+ end
+
class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
@@ -107,6 +117,21 @@ module ActiveRecord
end
end
+ class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc:
+ def initialize(klass, macro, association_name, options, possible_sources)
+ example_options = options.dup
+ example_options[:source] = possible_sources.first
+
+ super("Ambiguous source reflection for through association. Please " \
+ "specify a :source directive on your declaration like:\n" \
+ "\n" \
+ " class #{klass} < ActiveRecord::Base\n" \
+ " #{macro} :#{association_name}, #{example_options}\n" \
+ " end"
+ )
+ end
+ end
+
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
@@ -224,6 +249,11 @@ module ActiveRecord
autoload :AliasTracker
end
+ def self.eager_load!
+ super
+ Preloader.eager_load!
+ end
+
# Returns the association instance for the given name, instantiating it if it doesn't already exist
def association(name) #:nodoc:
association = association_instance_get(name)
@@ -255,11 +285,11 @@ module ActiveRecord
private
# Clears out the association cache.
- def clear_association_cache # :nodoc:
+ def clear_association_cache
@association_cache.clear if persisted?
end
- def init_internals # :nodoc:
+ def init_internals
@association_cache = {}
super
end
@@ -349,23 +379,23 @@ module ActiveRecord
#
# === Overriding generated methods
#
- # Association methods are generated in a module that is included into the model class,
- # which allows you to easily override with your own methods and call the original
- # generated method with +super+. For example:
+ # Association methods are generated in a module included into the model
+ # class, making overrides easy. The original generated method can thus be
+ # called with +super+:
#
# class Car < ActiveRecord::Base
# belongs_to :owner
# belongs_to :old_owner
+ #
# def owner=(new_owner)
# self.old_owner = self.owner
# super
# end
# end
#
- # If your model class is <tt>Project</tt>, then the module is
- # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is
- # included in the model class immediately after the (anonymous) generated attributes methods
- # module, meaning an association will override the methods for an attribute with the same name.
+ # The association methods module is included immediately after the
+ # generated attributes methods module, meaning an association will
+ # override the methods for an attribute with the same name.
#
# == Cardinality and associations
#
@@ -1617,6 +1647,9 @@ module ActiveRecord
# +:inverse_of+ to avoid an extra query during validation.
# NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If
# you don't want to have association presence validated, use <tt>optional: true</tt>.
+ # [:default]
+ # Provide a callable (i.e. proc or lambda) to specify that the association should
+ # be initialized with a particular record before validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
@@ -1630,6 +1663,7 @@ module ActiveRecord
# belongs_to :comment, touch: true
# belongs_to :company, touch: :employees_last_updated_at
# belongs_to :user, optional: true
+ # belongs_to :account, default: -> { company.account }
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1797,7 +1831,7 @@ module ActiveRecord
builder = Builder::HasAndBelongsToMany.new name, self, options
- join_model = builder.through_model
+ join_model = ActiveSupport::Deprecation.silence { builder.through_model }
const_set join_model.name, join_model
private_constant join_model.name
@@ -1826,8 +1860,8 @@ module ActiveRecord
hm_options[k] = options[k] if options.key? k
end
- has_many name, scope, hm_options, &extension
- self._reflections[name.to_s].parent_reflection = habtm_reflection
+ ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension }
+ _reflections[name.to_s].parent_reflection = habtm_reflection
end
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 84d0493a60..1cb2b2d7c6 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -83,7 +83,7 @@ module ActiveRecord
end
def scope
- target_scope.merge(association_scope)
+ target_scope.merge!(association_scope)
end
# The scope for this association.
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 12f8c1ccd4..120d75416c 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -25,7 +25,7 @@ module ActiveRecord
chain_head, chain_tail = get_chain(reflection, association, alias_tracker)
scope.extending! Array(reflection.options[:extend])
- add_constraints(scope, owner, klass, reflection, chain_head, chain_tail)
+ add_constraints(scope, owner, reflection, chain_head, chain_tail)
end
def join_type
@@ -49,6 +49,8 @@ module ActiveRecord
binds
end
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
+ # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :value_transformation
@@ -58,8 +60,8 @@ module ActiveRecord
table.create_join(table, table.create_on(constraint), join_type)
end
- def last_chain_scope(scope, table, reflection, owner, association_klass)
- join_keys = reflection.join_keys(association_klass)
+ def last_chain_scope(scope, table, reflection, owner)
+ join_keys = reflection.join_keys
key = join_keys.key
foreign_key = join_keys.foreign_key
@@ -78,8 +80,8 @@ module ActiveRecord
value_transformation.call(value)
end
- def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
- join_keys = reflection.join_keys(association_klass)
+ def next_chain_scope(scope, table, reflection, foreign_table, next_reflection)
+ join_keys = reflection.join_keys
key = join_keys.key
foreign_key = join_keys.foreign_key
@@ -118,25 +120,25 @@ module ActiveRecord
[runtime_reflection, previous_reflection]
end
- def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail)
+ def add_constraints(scope, owner, refl, chain_head, chain_tail)
owner_reflection = chain_tail
table = owner_reflection.alias_name
- scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass)
+ scope = last_chain_scope(scope, table, owner_reflection, owner)
reflection = chain_head
while reflection
table = reflection.alias_name
+ next_reflection = reflection.next
unless reflection == chain_tail
- next_reflection = reflection.next
foreign_table = next_reflection.alias_name
- scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
+ scope = next_chain_scope(scope, table, reflection, foreign_table, next_reflection)
end
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
reflection.constraints.each do |scope_chain_item|
- item = eval_scope(reflection.klass, scope_chain_item, owner)
+ item = eval_scope(reflection.klass, table, scope_chain_item, owner)
if scope_chain_item == refl.scope
scope.merge! item.except(:where, :includes)
@@ -151,14 +153,15 @@ module ActiveRecord
scope.order_values |= item.order_values
end
- reflection = reflection.next
+ reflection = next_reflection
end
scope
end
- def eval_scope(klass, scope, owner)
- klass.unscoped.instance_exec(owner, &scope)
+ def eval_scope(klass, table, scope, owner)
+ predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table))
+ ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope)
end
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 64b2311911..a2432e389a 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -21,6 +21,10 @@ module ActiveRecord
self.target = record
end
+ def default(&block)
+ writer(instance_exec(&block)) if reader.nil?
+ end
+
def reset
super
@updated = false
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index a1609ab0fb..2b9dd8aae8 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def self.valid_options(options)
- super + [:polymorphic, :touch, :counter_cache, :optional]
+ super + [:polymorphic, :touch, :counter_cache, :optional, :default]
end
def self.valid_dependent_options
@@ -16,6 +16,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
super
add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
add_touch_callbacks(model, reflection) if reflection.options[:touch]
+ add_default_callbacks(model, reflection) if reflection.options[:default]
end
def self.define_accessors(mixin, reflection)
@@ -118,6 +119,12 @@ module ActiveRecord::Associations::Builder # :nodoc:
model.after_destroy callback.(:changes_to_save)
end
+ def self.add_default_callbacks(model, reflection)
+ model.before_validation lambda { |o|
+ o.association(reflection.name).default(&reflection.options[:default])
+ }
+ end
+
def self.add_destroy_callbacks(model, reflection)
model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 42a90b449c..6b71826431 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
@@ -78,9 +78,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
private
- def self.suppress_composite_primary_key(pk)
- pk unless pk.is_a?(Array)
- end
+ def self.suppress_composite_primary_key(pk)
+ pk unless pk.is_a?(Array)
+ end
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index bb96202a22..7732b63af6 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -8,7 +8,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.define_accessors(model, reflection)
super
- define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
+ mixin = model.generated_association_methods
+ name = reflection.name
+
+ define_constructors(mixin, name) if reflection.constructable?
+
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def reload_#{name}
+ association(:#{name}).force_reload_reader
+ end
+ CODE
end
# Defines the (build|create)_association methods for belongs_to or has_one association
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index b2cf4713bb..77282e6463 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -25,26 +25,12 @@ module ActiveRecord
# +load_target+ and the +loaded+ flag are your friends.
class CollectionAssociation < Association #:nodoc:
# Implements the reader method, e.g. foo.items for Foo.has_many :items
- def reader(force_reload = false)
- if force_reload
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing an argument to force an association to reload is now
- deprecated and will be removed in Rails 5.1. Please call `reload`
- on the result collection proxy instead.
- MSG
-
- klass.uncached { reload }
- elsif stale_target?
+ def reader
+ if stale_target?
reload
end
- if null_scope?
- # Cache the proxy separately before the owner has an id
- # or else a post-save proxy will still lack the id
- @null_proxy ||= CollectionProxy.create(klass, self)
- else
- @proxy ||= CollectionProxy.create(klass, self)
- end
+ CollectionProxy.create(klass, self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -55,9 +41,7 @@ module ActiveRecord
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
def ids_reader
if loaded?
- load_target.map do |record|
- record.send(reflection.association_primary_key)
- end
+ target.pluck(reflection.association_primary_key)
else
@association_ids ||= (
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
@@ -68,13 +52,17 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_type = reflection.primary_key_type
+ pk_type = reflection.association_primary_key_type
ids = Array(ids).reject(&:blank?)
ids.map! { |i| pk_type.cast(i) }
records = klass.where(reflection.association_primary_key => ids).index_by do |r|
r.send(reflection.association_primary_key)
- end.values_at(*ids)
- replace(records)
+ end.values_at(*ids).compact
+ if records.size != ids.size
+ klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key)
+ else
+ replace(records)
+ end
end
def reset
@@ -295,23 +283,35 @@ module ActiveRecord
def replace_on_target(record, index, skip_callbacks)
callback(:before_add, record) unless skip_callbacks
- yield(record) if block_given?
+ begin
+ if index
+ record_was = target[index]
+ target[index] = record
+ else
+ target << record
+ end
- if index
- @target[index] = record
- else
- append_record(record)
+ set_inverse_instance(record)
+
+ yield(record) if block_given?
+ rescue
+ if index
+ target[index] = record_was
+ else
+ target.delete(record)
+ end
+
+ raise
end
callback(:after_add, record) unless skip_callbacks
- set_inverse_instance(record)
record
end
- def scope(opts = {})
- scope = super()
- scope.none! if opts.fetch(:nullify, true) && null_scope?
+ def scope
+ scope = super
+ scope.none! if null_scope?
scope
end
@@ -498,10 +498,6 @@ module ActiveRecord
load_target.select { |r| ids.include?(r.id.to_s) }
end
end
-
- def append_record(record)
- @target << record unless @target.include?(record)
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 35a98d7090..5d6676f0df 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -28,12 +28,9 @@ module ActiveRecord
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class CollectionProxy < Relation
- delegate :exists?, :update_all, :arel, to: :scope
-
def initialize(klass, association) #:nodoc:
@association = association
super klass, klass.arel_table, klass.predicate_builder
- merge! association.scope(nullify: false)
end
def target
@@ -81,7 +78,7 @@ module ActiveRecord
# # #<Pet id: nil, name: "Choo-Choo">
# # ]
#
- # person.pets.select(:id, :name )
+ # person.pets.select(:id, :name)
# # => [
# # #<Pet id: 1, name: "Fancy-Fancy">,
# # #<Pet id: 2, name: "Spook">,
@@ -746,10 +743,6 @@ module ActiveRecord
# # ]
#--
- def uniq
- load_target.uniq
- end
-
def calculate(operation, column_name)
null_scope? ? scope.calculate(operation, column_name) : super
end
@@ -956,19 +949,10 @@ module ActiveRecord
@association
end
- # We don't want this object to be put on the scoping stack, because
- # that could create an infinite loop where we call an @association
- # method, which gets the current scope, which is this object, which
- # delegates to @association, and so on.
- def scoping
- @association.scope.scoping { yield }
- end
-
# Returns a <tt>Relation</tt> object for the records in this association
def scope
- @association.scope
+ @scope ||= @association.scope
end
- alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
# contain the same number of elements and if each element is equal
@@ -1100,6 +1084,7 @@ module ActiveRecord
# person.pets(true) # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
+ @scope = nil
proxy_association.reload
self
end
@@ -1121,12 +1106,22 @@ module ActiveRecord
# person.pets # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reset
+ @scope = nil
proxy_association.reset
proxy_association.reset_scope
self
end
- protected
+ delegate_methods = [
+ QueryMethods,
+ SpawnMethods,
+ ].flat_map { |klass|
+ klass.public_instance_methods(false)
+ } - self.public_instance_methods(false) - [:select] + [:scoping]
+
+ delegate(*delegate_methods, to: :scope)
+
+ private
def find_nth_with_limit(index, limit)
load_target if find_from_target?
@@ -1138,8 +1133,6 @@ module ActiveRecord
super
end
- private
-
def null_scope?
@association.null_scope?
end
@@ -1151,6 +1144,19 @@ module ActiveRecord
def exec_queries
load_target
end
+
+ def respond_to_missing?(method, _)
+ scope.respond_to?(method) || super
+ end
+
+ def method_missing(method, *args, &block)
+ if scope.respond_to?(method) && scope.extending_values.any?
+ extend(*scope.extending_values)
+ public_send(method, *args, &block)
+ else
+ super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 742cd25509..b413eb3f9c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -16,14 +16,7 @@ module ActiveRecord
when :restrict_with_error
unless empty?
record = owner.class.human_attribute_name(reflection.name).downcase
- message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil
- if message
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1.
- Please use `:'restrict_dependent_destroy.has_many'` instead.
- MESSAGE
- end
- owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record)
+ owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record)
throw(:abort)
end
@@ -38,7 +31,6 @@ module ActiveRecord
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
- set_inverse_instance(record)
if raise
record.save!(validate: validate)
@@ -108,7 +100,7 @@ module ActiveRecord
end
def delete_or_nullify_all_records(method)
- count = delete_count(method, self.scope)
+ count = delete_count(method, scope)
update_counter(-count)
end
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 8c90aea975..c4a7fe4432 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -38,10 +38,12 @@ module ActiveRecord
def insert_record(record, validate = true, raise = false)
ensure_not_nested
- if raise
- record.save!(validate: validate)
- else
- return unless record.save(validate: validate)
+ if record.new_record? || record.has_changes_to_save?
+ if raise
+ record.save!(validate: validate)
+ else
+ return unless record.save(validate: validate)
+ end
end
save_through_record(record)
@@ -206,10 +208,6 @@ module ActiveRecord
def invertible_for?(record)
false
end
-
- def append_record(record)
- @target << record
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 21bd668dff..8458253ff8 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -12,14 +12,7 @@ module ActiveRecord
when :restrict_with_error
if load_target
record = owner.class.human_attribute_name(reflection.name).downcase
- message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil
- if message
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1.
- Please use `:'restrict_dependent_destroy.has_one'` instead.
- MESSAGE
- end
- owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record)
+ owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record)
throw(:abort)
end
@@ -92,7 +85,7 @@ module ActiveRecord
if target.persisted? && owner.persisted? && !target.save
set_owner_attributes(target)
- raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
+ raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \
"The record failed to save after its foreign key was set to nil."
end
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 604904abcc..1183bdf6f4 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -22,6 +22,10 @@ module ActiveRecord
elsif record
attributes = construct_join_attributes(record)
+ if through_record && through_record.destroyed?
+ through_record = through_proxy.tap(&:reload).target
+ end
+
if through_record
through_record.update(attributes)
elsif owner.new_record?
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 4cd1e64c3d..8995b1e352 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -32,7 +32,7 @@ module ActiveRecord
@alias_cache[node][column]
end
- class Table < Struct.new(:node, :columns) # :nodoc:
+ Table = Struct.new(:node, :columns) do # :nodoc:
def table
Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
end
@@ -171,7 +171,7 @@ module ActiveRecord
chain = child.reflection.chain
foreign_table = parent.table
foreign_klass = parent.base_klass
- child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
+ child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain)
end
def make_outer_joins(parent, child)
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 a5705951f3..97cfec0302 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -23,50 +23,27 @@ module ActiveRecord
JoinInformation = Struct.new :joins, :binds
- def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ def join_constraints(foreign_table, foreign_klass, join_type, tables, chain)
joins = []
binds = []
tables = tables.reverse
- scope_chain_index = 0
- scope_chain = scope_chain.reverse
-
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
chain.reverse_each do |reflection|
table = tables.shift
klass = reflection.klass
- join_keys = reflection.join_keys(klass)
+ join_keys = reflection.join_keys
key = join_keys.key
foreign_key = join_keys.foreign_key
constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table))
- scope_chain_items = scope_chain[scope_chain_index].map do |item|
- if item.is_a?(Relation)
- item
- else
- ActiveRecord::Relation.create(klass, table, predicate_builder)
- .instance_exec(node, &item)
- end
- end
- scope_chain_index += 1
-
- klass_scope =
- if klass.current_scope
- klass.current_scope.clone.tap { |scope|
- scope.joins_values = []
- }
- else
- relation = ActiveRecord::Relation.create(
- klass,
- table,
- predicate_builder,
- )
- klass.send(:build_default_scope, relation)
- end
+ scope_chain_items = reflection.join_scopes(table, predicate_builder)
+ klass_scope = reflection.klass_join_scope(table, predicate_builder)
+
scope_chain_items.concat [klass_scope].compact
rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index e386cc0e4c..91580a28d0 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -2,16 +2,8 @@ module ActiveRecord
module Associations
class SingularAssociation < Association #:nodoc:
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
- def reader(force_reload = false)
- if force_reload && klass
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing an argument to force an association to reload is now
- deprecated and will be removed in Rails 5.1. Please call `reload`
- on the parent object instead.
- MSG
-
- klass.uncached { reload }
- elsif !loaded? || stale_target?
+ def reader
+ if !loaded? || stale_target?
reload
end
@@ -30,6 +22,13 @@ module ActiveRecord
record
end
+ # Implements the reload reader method, e.g. foo.reload_bar for
+ # Foo.has_one :bar
+ def force_reload_reader
+ klass.uncached { reload }
+ target
+ end
+
private
def create_scope
@@ -51,6 +50,8 @@ module ActiveRecord
sc.execute(binds, klass, conn) do |record|
set_inverse_instance record
end.first
+ rescue ::RangeError
+ nil
end
def replace(record)
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index f4129edc5a..6b87993ba3 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module ThroughAssociation #:nodoc:
delegate :source_reflection, :through_reflection, to: :reflection
- protected
+ private
# We merge in these scopes for two reasons:
#
@@ -21,8 +21,6 @@ module ActiveRecord
scope
end
- private
-
# Construct attributes for :through pointing to owner and associate. This is used by the
# methods which create and delete records on the association.
#
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 0b08c2a39b..38281158d8 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -128,11 +128,22 @@ 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
+ else
+ _original_value_for_database
+ end
+ end
+
+ private
def initialize_dup(other)
if defined?(@value) && @value.duplicable?
@value = @value.dup
@@ -143,14 +154,6 @@ module ActiveRecord
assigned? && type.changed?(original_value, value, value_before_type_cast)
end
- def original_value_for_database
- if assigned?
- original_attribute.original_value_for_database
- else
- _original_value_for_database
- end
- end
-
def _original_value_for_database
type.serialize(original_value)
end
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
index a4e2c2ec85..57f8bbed76 100644
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ b/activerecord/lib/active_record/attribute/user_provided_default.rb
@@ -20,6 +20,8 @@ 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
attr_reader :user_provided_value
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 9843e0ca66..d0dfca0cac 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -12,7 +12,7 @@ module ActiveRecord
private
- def _assign_attributes(attributes) # :nodoc:
+ def _assign_attributes(attributes)
multi_parameter_attributes = {}
nested_parameter_attributes = {}
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 340dfe11cf..c39e9ce4c5 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -8,12 +8,34 @@ module ActiveRecord
end
module ClassMethods # :nodoc:
+ # This method is an internal API used to create class macros such as
+ # +serialize+, and features like time zone aware attributes.
+ #
+ # Used to wrap the type of an attribute in a new type.
+ # When the schema for a model is loaded, attributes with the same name as
+ # +column_name+ will have their type yielded to the given block. The
+ # return value of that block will be used instead.
+ #
+ # Subsequent calls where +column_name+ and +decorator_name+ are the same
+ # will override the previous decorator, not decorate twice. This can be
+ # used to create idempotent class macros like +serialize+
def decorate_attribute_type(column_name, decorator_name, &block)
matcher = ->(name, _) { name == column_name.to_s }
key = "_#{column_name}_#{decorator_name}"
decorate_matching_attribute_types(matcher, key, &block)
end
+ # This method is an internal API used to create higher level features like
+ # time zone aware attributes.
+ #
+ # When the schema for a model is loaded, +matcher+ will be called for each
+ # attribute with its name and type. If the matcher returns a truthy value,
+ # the type will then be yielded to the given block, and the return value
+ # of that block will replace the type.
+ #
+ # Subsequent calls to this method with the same value for +decorator_name+
+ # will replace the previous decorator, not decorate twice. This can be
+ # used to ensure that class macros are idempotent.
def decorate_matching_attribute_types(matcher, decorator_name, &block)
reload_schema_from_cache
decorator_name = decorator_name.to_s
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 1ed1deec55..ebe06566cc 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -394,28 +394,21 @@ module ActiveRecord
protected
- def clone_attribute_value(reader_method, attribute_name) # :nodoc:
- value = send(reader_method, attribute_name)
- value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
- value
+ def attribute_method?(attr_name) # :nodoc:
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
+ defined?(@attributes) && @attributes.key?(attr_name)
end
- def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
+ private
+
+ def arel_attributes_with_values_for_create(attribute_names)
arel_attributes_with_values(attributes_for_create(attribute_names))
end
- def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
+ def arel_attributes_with_values_for_update(attribute_names)
arel_attributes_with_values(attributes_for_update(attribute_names))
end
- def attribute_method?(attr_name) # :nodoc:
- # We check defined? because Syck calls respond_to? before actually calling initialize.
- defined?(@attributes) && @attributes.key?(attr_name)
- end
-
- private
-
# 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)
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index b22190455a..6aa414ba6b 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -50,7 +50,7 @@ module ActiveRecord
super.tap do
@previous_mutation_tracker = nil
clear_mutation_trackers
- @changed_attributes = HashWithIndifferentAccess.new
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
end
end
@@ -70,13 +70,13 @@ module ActiveRecord
def changes_applied
@previous_mutation_tracker = mutation_tracker
- @changed_attributes = HashWithIndifferentAccess.new
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
clear_mutation_trackers
end
def clear_changes_information
@previous_mutation_tracker = nil
- @changed_attributes = HashWithIndifferentAccess.new
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
clear_mutation_trackers
end
@@ -100,14 +100,14 @@ module ActiveRecord
if defined?(@cached_changed_attributes)
@cached_changed_attributes
else
- emit_warning_if_needed("changed_attributes", "attributes_in_database")
+ emit_warning_if_needed("changed_attributes", "saved_changes.transform_values(&:first)")
super.reverse_merge(mutation_tracker.changed_values).freeze
end
end
def changes
cache_changed_attributes do
- emit_warning_if_needed("changes", "changes_to_save")
+ emit_warning_if_needed("changes", "saved_changes")
super
end
end
@@ -212,22 +212,27 @@ module ActiveRecord
end
def attribute_was(*)
- emit_warning_if_needed("attribute_was", "attribute_in_database")
+ emit_warning_if_needed("attribute_was", "attribute_before_last_save")
super
end
def attribute_change(*)
- emit_warning_if_needed("attribute_change", "attribute_change_to_be_saved")
+ emit_warning_if_needed("attribute_change", "saved_change_to_attribute")
super
end
def attribute_changed?(*)
- emit_warning_if_needed("attribute_changed?", "will_save_change_to_attribute?")
+ emit_warning_if_needed("attribute_changed?", "saved_change_to_attribute?")
+ super
+ end
+
+ def changed?(*)
+ emit_warning_if_needed("changed?", "saved_changes?")
super
end
def changed(*)
- emit_warning_if_needed("changed", "changed_attribute_names_to_save")
+ emit_warning_if_needed("changed", "saved_changes.keys")
super
end
@@ -270,7 +275,17 @@ module ActiveRecord
def attribute_will_change!(attr_name)
super
- mutations_from_database.force_change(attr_name)
+ if self.class.has_attribute?(attr_name)
+ mutations_from_database.force_change(attr_name)
+ else
+ ActiveSupport::Deprecation.warn(<<-EOW.squish)
+ #{attr_name} is not an attribute known to Active Record.
+ This behavior is deprecated and will be removed in the next
+ version of Rails. If you'd like #{attr_name} to be managed
+ by Active Record, add `attribute :#{attr_name} to your class.
+ EOW
+ mutations_from_database.deprecated_force_change(attr_name)
+ end
end
def _update_record(*)
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 287367f92a..2f32caa257 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -9,7 +9,7 @@ module ActiveRecord
# available.
def to_key
sync_with_transaction_state
- key = self.id
+ key = id
[key] if key
end
@@ -50,7 +50,7 @@ module ActiveRecord
attribute_in_database(self.class.primary_key)
end
- protected
+ private
def attribute_method?(attr_name)
attr_name == "id" || super
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 30f7750884..fdc4bf6621 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -4,7 +4,7 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- protected
+ private
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
@@ -48,8 +48,13 @@ module ActiveRecord
# it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name, &block)
- name = attr_name.to_s
- name = self.class.primary_key if name == "id".freeze
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+
+ name = self.class.primary_key if name == "id".freeze && self.class.primary_key
_read_attribute(name, &block)
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 945192fe04..4d9aff76cc 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -54,7 +54,7 @@ module ActiveRecord
elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
class_name_or_coder
else
- Coders::YAMLColumn.new(class_name_or_coder)
+ Coders::YAMLColumn.new(attr_name, class_name_or_coder)
end
decorate_attribute_type(attr_name, :serialize) do |type|
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 500d903857..321d039ed4 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,5 +1,3 @@
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
@@ -63,7 +61,7 @@ module ActiveRecord
self.skip_time_zone_conversion_for_attributes = []
class_attribute :time_zone_aware_types, instance_writer: false
- self.time_zone_aware_types = [:datetime, :not_explicitly_configured]
+ self.time_zone_aware_types = [:datetime, :time]
end
module ClassMethods
@@ -86,29 +84,8 @@ module ActiveRecord
def create_time_zone_conversion_attribute?(name, cast_type)
enabled_for_column = time_zone_aware_attributes &&
!skip_time_zone_conversion_for_attributes.include?(name.to_sym)
- result = enabled_for_column &&
- time_zone_aware_types.include?(cast_type.type)
-
- if enabled_for_column &&
- !result &&
- cast_type.type == :time &&
- time_zone_aware_types.include?(:not_explicitly_configured)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- Time columns will become time zone aware in Rails 5.1. This
- still causes `String`s to be parsed as if they were in `Time.zone`,
- and `Time`s to be converted to `Time.zone`.
-
- To keep the old behavior, you must add the following to your initializer:
-
- config.active_record.time_zone_aware_types = [:datetime]
-
- To silence this deprecation warning, add the following:
-
- config.active_record.time_zone_aware_types = [:datetime, :time]
- MESSAGE
- end
- result
+ enabled_for_column && time_zone_aware_types.include?(cast_type.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index f65c297e01..fe0e01db28 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
module ClassMethods
- protected
+ private
def define_method_attribute=(name)
safe_name = name.unpack("h*".freeze).first
@@ -29,7 +29,13 @@ module ActiveRecord
# specified +value+. Empty strings for Integer and Float columns are
# turned into +nil+.
def write_attribute(attr_name, value)
- write_attribute_with_type_cast(attr_name, value, true)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+
+ write_attribute_with_type_cast(name, value, true)
end
def raw_write_attribute(attr_name, value) # :nodoc:
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
index db86b2b294..4de993e169 100644
--- a/activerecord/lib/active_record/attribute_mutation_tracker.rb
+++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb
@@ -5,6 +5,7 @@ module ActiveRecord
def initialize(attributes)
@attributes = attributes
@forced_changes = Set.new
+ @deprecated_forced_changes = Set.new
end
def changed_values
@@ -31,7 +32,7 @@ module ActiveRecord
end
def any_changes?
- attr_names.any? { |attr| changed?(attr) }
+ attr_names.any? { |attr| changed?(attr) } || deprecated_forced_changes.any?
end
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
@@ -60,9 +61,15 @@ module ActiveRecord
forced_changes << attr_name.to_s
end
+ def deprecated_force_change(attr_name)
+ deprecated_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
+ attr_reader :attributes, :forced_changes, :deprecated_forced_changes
private
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index 5bde1f107c..66b278219a 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -98,6 +98,8 @@ module ActiveRecord
attributes == other.attributes
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
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index 661f996e1a..2f624d32af 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -90,6 +90,8 @@ module ActiveRecord
@materialized = true
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
diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
index c86cfc4263..899de14792 100644
--- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
+++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
@@ -31,6 +31,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 :default_types
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index dcbfca1c04..75f5ba3a96 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -191,7 +191,7 @@ module ActiveRecord
# tracking is performed. The methods +changed?+ and +changed_in_place?+
# will be called from ActiveModel::Dirty. See the documentation for those
# methods in ActiveModel::Type::Value for more details.
- def attribute(name, cast_type, **options)
+ def attribute(name, cast_type = Type::Value.new, **options)
name = name.to_s
reload_schema_from_cache
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index b343332bae..6bccbc06cd 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -328,9 +328,9 @@ module ActiveRecord
def association_valid?(reflection, record, index = nil)
return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
- validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
+ context = validation_context unless [:create, :update].include?(validation_context)
- unless valid = record.valid?(validation_context)
+ unless valid = record.valid?(context)
if reflection.options[:autosave]
indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors)
@@ -383,6 +383,9 @@ module ActiveRecord
if association = association_instance_get(reflection.name)
autosave = reflection.options[:autosave]
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
@@ -408,9 +411,6 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
end
-
- # reconstruct the scope now that we know the owner's id
- association.reset_scope if association.respond_to?(:reset_scope)
end
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index f2e3912c6e..eb44887e18 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -225,6 +225,55 @@ module ActiveRecord
#
# This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available.
#
+ # Also, there are cases when you want several callbacks of the same type to
+ # be executed in order.
+ #
+ # For example:
+ #
+ # class Topic
+ # has_many :children
+ #
+ # after_save :log_children
+ # after_save :do_something_else
+ #
+ # private
+ #
+ # def log_chidren
+ # # Child processing
+ # end
+ #
+ # def do_something_else
+ # # Something else
+ # end
+ # end
+ #
+ # In this case the +log_children+ gets executed before +do_something_else+.
+ # The same applies to all non-transactional callbacks.
+ #
+ # In case there are multiple transactional callbacks as seen below, the order
+ # is reversed.
+ #
+ # For example:
+ #
+ # class Topic
+ # has_many :children
+ #
+ # after_commit :log_children
+ # after_commit :do_something_else
+ #
+ # private
+ #
+ # def log_chidren
+ # # Child processing
+ # end
+ #
+ # def do_something_else
+ # # Something else
+ # end
+ # end
+ #
+ # In this case the +do_something_else+ gets executed before +log_children+.
+ #
# == \Transactions
#
# The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!],
@@ -283,15 +332,15 @@ module ActiveRecord
private
- def create_or_update(*) #:nodoc:
+ def create_or_update(*)
_run_save_callbacks { super }
end
- def _create_record #:nodoc:
+ def _create_record
_run_create_callbacks { super }
end
- def _update_record(*) #:nodoc:
+ def _update_record(*)
_run_update_callbacks { super }
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 3a04a10fc9..9c52a31b95 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -5,7 +5,8 @@ module ActiveRecord
class YAMLColumn # :nodoc:
attr_accessor :object_class
- def initialize(object_class = Object)
+ def initialize(attr_name, object_class = Object)
+ @attr_name = attr_name
@object_class = object_class
check_arity_of_constructor
end
@@ -13,7 +14,7 @@ module ActiveRecord
def dump(obj)
return if obj.nil?
- assert_valid_value(obj)
+ assert_valid_value(obj, action: "dump")
YAML.dump obj
end
@@ -22,27 +23,25 @@ module ActiveRecord
return yaml unless yaml.is_a?(String) && /^---/.match?(yaml)
obj = YAML.load(yaml)
- assert_valid_value(obj)
+ assert_valid_value(obj, action: "load")
obj ||= object_class.new if object_class != Object
obj
end
- def assert_valid_value(obj)
+ def assert_valid_value(obj, action:)
unless obj.nil? || obj.is_a?(object_class)
raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
+ "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
end
end
private
def check_arity_of_constructor
- begin
- load(nil)
- rescue ArgumentError
- raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
- end
+ load(nil)
+ rescue ArgumentError
+ raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index c9f907b281..3f2e86a98d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -353,6 +353,16 @@ module ActiveRecord
@threads_blocking_new_connections = 0
@available = ConnectionLeasingQueue.new self
+
+ @lock_thread = false
+ end
+
+ def lock_thread=(lock_thread)
+ if lock_thread
+ @lock_thread = Thread.current
+ else
+ @lock_thread = nil
+ end
end
# Retrieve the connection associated with the current thread, or call
@@ -361,7 +371,7 @@ module ActiveRecord
# #connection can be called any number of times; the connection is
# held in a cache keyed by a thread.
def connection
- @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout
+ @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
end
# Returns true if there is an open connection being used for the current thread.
@@ -445,8 +455,6 @@ module ActiveRecord
# connections in the pool within a timeout interval (default duration is
# <tt>spec.config[:checkout_timeout] * 2</tt> seconds).
def clear_reloadable_connections(raise_on_acquisition_timeout = true)
- num_new_conns_required = 0
-
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@connections.each do |conn|
@@ -457,24 +465,9 @@ module ActiveRecord
conn.disconnect! if conn.requires_reloading?
end
@connections.delete_if(&:requires_reloading?)
-
@available.clear
-
- if @connections.size < @size
- # because of the pruning done by this method, we might be running
- # low on connections, while threads stuck in queue are helpless
- # (not being able to establish new connections for themselves),
- # see also more detailed explanation in +remove+
- num_new_conns_required = num_waiting_in_queue - @connections.size
- end
-
- @connections.each do |conn|
- @available.add conn
- end
end
end
-
- bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0
end
# Clears the cache which maps classes and re-connects connections that
@@ -705,9 +698,26 @@ module ActiveRecord
yield
ensure
+ num_new_conns_required = 0
+
synchronize do
@threads_blocking_new_connections -= 1
+
+ if @threads_blocking_new_connections.zero?
+ @available.clear
+
+ num_new_conns_required = num_waiting_in_queue
+
+ @connections.each do |conn|
+ next if conn.in_use?
+
+ @available.add conn
+ num_new_conns_required -= 1
+ end
+ end
end
+
+ bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0
end
# Acquire a connection by one of 1) immediately removing one
@@ -967,7 +977,7 @@ module ActiveRecord
end
def pool_from_any_process_for(spec_name)
- owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] }
+ owner_to_pool = @owner_to_pool.values.reverse.find { |v| v[spec_name] }
owner_to_pool && owner_to_pool[spec_name]
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index faccd1d641..769f488469 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -10,9 +10,9 @@ module ActiveRecord
def to_sql(arel, binds = [])
if arel.respond_to?(:ast)
collected = visitor.accept(arel.ast, collector)
- collected.compile(binds, self)
+ collected.compile(binds, self).freeze
else
- arel
+ arel.dup.freeze
end
end
@@ -51,8 +51,7 @@ module ActiveRecord
# Returns a single value from a record
def select_value(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- if result = select_rows(to_sql(arel, binds), name, binds).first
+ if result = select_rows(arel, name, binds).first
result.first
end
end
@@ -60,14 +59,13 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- select_rows(to_sql(arel, binds), name, binds).map(&:first)
+ select_rows(arel, name, binds).map(&:first)
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil, binds = [])
- exec_query(sql, name, binds).rows
+ def select_rows(arel, name = nil, binds = [])
+ select_all(arel, name, binds).rows
end
# Executes the SQL statement in the context of this connection and returns
@@ -126,22 +124,16 @@ module ActiveRecord
id_value || last_inserted_id(value)
end
alias create insert
- alias insert_sql insert
- deprecate insert_sql: :insert
# Executes the update statement and returns the number of rows affected.
def update(arel, name = nil, binds = [])
exec_update(to_sql(arel, binds), name, binds)
end
- alias update_sql update
- deprecate update_sql: :update
# Executes the delete statement and returns the number of rows affected.
def delete(arel, name = nil, binds = [])
exec_delete(to_sql(arel, binds), name, binds)
end
- alias delete_sql delete
- deprecate delete_sql: :delete
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
@@ -334,17 +326,12 @@ module ActiveRecord
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# The +limit+ may be anything that can evaluate to a string via #to_s. It
- # should look like an integer, or a comma-delimited list of integers, or
- # an Arel SQL literal.
+ # should look like an integer, or an Arel SQL literal.
#
# Returns Integer and Arel::Nodes::SqlLiteral limits as is.
- # Returns the sanitized limit parameter, either as an integer, or as a
- # string which contains a comma-delimited list of integers.
def sanitize_limit(limit)
if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
limit
- elsif limit.to_s.include?(",")
- Arel.sql limit.to_s.split(",").map { |i| Integer(i) }.join(",")
else
Integer(limit)
end
@@ -360,7 +347,7 @@ module ActiveRecord
end
alias join_to_delete join_to_update
- protected
+ private
# 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/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 7eab7de5d3..e53ba4e666 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -83,7 +83,9 @@ module ActiveRecord
# the same SQL query and repeatedly return the same result each time, silently
# undermining the randomness you were expecting.
def clear_query_cache
- @query_cache.clear
+ @lock.synchronize do
+ @query_cache.clear
+ end
end
def select_all(arel, name = nil, binds = [], preparable: nil)
@@ -99,21 +101,23 @@ module ActiveRecord
private
def cache_sql(sql, name, binds)
- result =
- if @query_cache[sql].key?(binds)
- ActiveSupport::Notifications.instrument(
- "sql.active_record",
- sql: sql,
- binds: binds,
- name: name,
- connection_id: object_id,
- cached: true,
- )
- @query_cache[sql][binds]
- else
- @query_cache[sql][binds] = yield
- end
- result.dup
+ @lock.synchronize do
+ result =
+ if @query_cache[sql].key?(binds)
+ ActiveSupport::Notifications.instrument(
+ "sql.active_record",
+ sql: sql,
+ binds: binds,
+ name: name,
+ connection_id: object_id,
+ cached: true,
+ )
+ @query_cache[sql][binds]
+ else
+ @query_cache[sql][binds] = yield
+ end
+ result.dup
+ end
end
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index bbd52b8a91..cce8883076 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -1,22 +1,18 @@
require "active_support/core_ext/big_decimal/conversions"
+require "active_support/multibyte/chars"
module ActiveRecord
module ConnectionAdapters # :nodoc:
module Quoting
# Quotes the column value to help prevent
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
- def quote(value, column = nil)
- # records are quoted as their primary key
- return value.quoted_id if value.respond_to?(:quoted_id)
+ def quote(value)
+ value = id_value_for_database(value) if value.is_a?(Base)
- if column
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing a column to `quote` has been deprecated. It is only used
- for type casting, which should be handled elsewhere. See
- https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
- for more information.
- MSG
- value = type_cast_from_column(column, value)
+ if value.respond_to?(:quoted_id)
+ ActiveSupport::Deprecation.warn \
+ "Using #quoted_id is deprecated and will be removed in Rails 5.2."
+ return value.quoted_id
end
_quote(value)
@@ -26,6 +22,8 @@ module ActiveRecord
# SQLite does not understand dates, so this method will convert a Date
# to a String.
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
@@ -150,11 +148,24 @@ module ActiveRecord
quoted_date(value).sub(/\A2000-01-01 /, "")
end
- private
+ def quoted_binary(value) # :nodoc:
+ "'#{quote_string(value.to_s)}'"
+ end
- def type_casted_binds(binds)
+ def type_casted_binds(binds) # :nodoc:
+ if binds.first.is_a?(Array)
+ binds.map { |column, value| type_cast(value, column) }
+ else
binds.map { |attr| type_cast(attr.value_for_database) }
end
+ end
+
+ private
+ def id_value_for_database(value)
+ if primary_key = value.class.primary_key
+ value.instance_variable_get(:@attributes)[primary_key].value_for_database
+ end
+ end
def types_which_need_no_typecasting
[nil, Numeric, String]
@@ -162,7 +173,7 @@ module ActiveRecord
def _quote(value)
case value
- when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ when String, ActiveSupport::Multibyte::Chars
"'#{quote_string(value.to_s)}'"
when true then quoted_true
when false then quoted_false
@@ -170,6 +181,7 @@ module ActiveRecord
# BigDecimals need to be put in a non-normalized form and quoted.
when BigDecimal then value.to_s("F")
when Numeric, ActiveSupport::Duration then value.to_s
+ when Type::Binary::Data then quoted_binary(value)
when Type::Time::Value then "'#{quoted_time(value)}'"
when Date, Time then "'#{quoted_date(value)}'"
when Symbol then "'#{quote_string(value.to_s)}'"
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 322684672f..c48a4acff8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -15,9 +15,9 @@ 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?, :foreign_key_options, to: :@conn
+ :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?, :foreign_key_options
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options
private
@@ -29,7 +29,7 @@ module ActiveRecord
end
def visit_ColumnDefinition(o)
- o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale)
+ o.sql_type = type_to_sql(o.type, o.options)
column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
column_sql
@@ -49,7 +49,7 @@ module ActiveRecord
statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
end
- if supports_foreign_keys?
+ if supports_foreign_keys_in_create?
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
end
@@ -96,17 +96,7 @@ module ActiveRecord
end
def column_options(o)
- column_options = {}
- column_options[:null] = o.null unless o.null.nil?
- column_options[:default] = o.default unless o.default.nil?
- column_options[:column] = o
- column_options[:first] = o.first
- column_options[:after] = o.after
- column_options[:auto_increment] = o.auto_increment
- column_options[:primary_key] = o.primary_key
- column_options[:collation] = o.collation
- column_options[:comment] = o.comment
- column_options
+ o.options.merge(column: o)
end
def add_column_options!(sql, options)
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 83d1d7cd01..4682afc188 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -3,29 +3,37 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc:
- end
+ IndexDefinition = Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc:
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc:
+ ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc:
def primary_key?
- primary_key || type.to_sym == :primary_key
+ options[:primary_key]
end
- end
- class AddColumnDefinition < Struct.new(:column) # :nodoc:
- end
+ [:limit, :precision, :scale, :default, :null, :collation, :comment].each do |option_name|
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{option_name}
+ options[:#{option_name}]
+ end
- class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc:
+ def #{option_name}=(value)
+ options[:#{option_name}] = value
+ end
+ CODE
+ end
end
- class PrimaryKeyDefinition < Struct.new(:name) # :nodoc:
- end
+ AddColumnDefinition = Struct.new(:column) # :nodoc:
+
+ ChangeColumnDefinition = Struct.new(:column, :name) #:nodoc:
+
+ PrimaryKeyDefinition = Struct.new(:name) # :nodoc:
- class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ ForeignKeyDefinition = Struct.new(:from_table, :to_table, :options) do #:nodoc:
def name
options[:name]
end
@@ -71,7 +79,7 @@ module ActiveRecord
polymorphic: false,
index: true,
foreign_key: false,
- type: :integer,
+ type: :bigint,
**options
)
@name = name
@@ -100,22 +108,20 @@ 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
- def as_options(value, default = {})
- if value.is_a?(Hash)
- value
- else
- default
- end
+ def as_options(value)
+ value.is_a?(Hash) ? value : {}
end
def polymorphic_options
- as_options(polymorphic, options)
+ as_options(polymorphic)
end
def index_options
@@ -175,6 +181,7 @@ module ActiveRecord
:text,
:time,
:timestamp,
+ :virtual,
].each do |column_type|
module_eval <<-CODE, __FILE__, __LINE__ + 1
def #{column_type}(*args, **options)
@@ -355,33 +362,22 @@ module ActiveRecord
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
def references(*args, **options)
- args.each do |col|
- ReferenceDefinition.new(col, **options).add_to(self)
+ args.each do |ref_name|
+ ReferenceDefinition.new(ref_name, options).add_to(self)
end
end
alias :belongs_to :references
- def new_column_definition(name, type, options) # :nodoc:
+ def new_column_definition(name, type, **options) # :nodoc:
type = aliased_types(type.to_s, type)
- column = create_column_definition name, type
-
- column.limit = options[:limit]
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- column.first = options[:first]
- column.after = options[:after]
- column.auto_increment = options[:auto_increment]
- column.primary_key = type == :primary_key || options[:primary_key]
- column.collation = options[:collation]
- column.comment = options[:comment]
- column
+ options[:primary_key] ||= type == :primary_key
+ options[:null] = false if options[:primary_key]
+ create_column_definition(name, type, options)
end
private
- def create_column_definition(name, type)
- ColumnDefinition.new name, type
+ def create_column_definition(name, type, options)
+ ColumnDefinition.new(name, type, options)
end
def aliased_types(name, fallback)
@@ -589,8 +585,7 @@ module ActiveRecord
# t.belongs_to(:supplier, foreign_key: true)
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
- def references(*args)
- options = args.extract_options!
+ def references(*args, **options)
args.each do |ref_name|
@base.add_reference(name, ref_name, options)
end
@@ -603,8 +598,7 @@ module ActiveRecord
# t.remove_belongs_to(:supplier, polymorphic: true)
#
# See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference]
- def remove_references(*args)
- options = args.extract_options!
+ def remove_references(*args, **options)
args.each do |ref_name|
@base.remove_reference(name, ref_name, options)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index dabccc00bb..34036d8a1d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -7,13 +7,15 @@ module ActiveRecord
# Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
def column_spec(column)
- [schema_type(column), prepare_column_options(column)]
+ [schema_type_with_virtual(column), prepare_column_options(column)]
end
def column_spec_for_primary_key(column)
return {} if default_primary_key?(column)
spec = { id: schema_type(column).inspect }
spec.merge!(prepare_column_options(column).except!(:null))
+ spec[:default] ||= "nil" if explicit_primary_key_default?(column)
+ spec
end
# This can be overridden on an Adapter level basis to support other
@@ -49,14 +51,27 @@ module ActiveRecord
end
# Lists the valid migration options
- def migration_keys
- [:limit, :precision, :scale, :default, :null, :collation, :comment]
+ def migration_keys # :nodoc:
+ column_options_keys
end
+ deprecate :migration_keys
private
def default_primary_key?(column)
- schema_type(column) == :integer
+ schema_type(column) == :bigint
+ end
+
+ def explicit_primary_key_default?(column)
+ false
+ end
+
+ def schema_type_with_virtual(column)
+ if supports_virtual_columns? && column.virtual?
+ :virtual
+ else
+ schema_type(column)
+ end
end
def schema_type(column)
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 151629b02a..1cc10e3b93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -31,6 +31,8 @@ module ActiveRecord
# Returns the relation names useable to back Active Record models.
# For most adapters this means all #tables and #views.
def data_sources
+ select_values(data_source_sql, "SCHEMA")
+ rescue NotImplementedError
tables | views
end
@@ -39,12 +41,14 @@ module ActiveRecord
# data_source_exists?(:ebooks)
#
def data_source_exists?(name)
+ select_values(data_source_sql(name), "SCHEMA").any? if name.present?
+ rescue NotImplementedError
data_sources.include?(name.to_s)
end
# Returns an array of table names defined in the database.
- def tables(name = nil)
- raise NotImplementedError, "#tables is not implemented"
+ def tables
+ select_values(data_source_sql(type: "BASE TABLE"), "SCHEMA")
end
# Checks to see if the table +table_name+ exists on the database.
@@ -52,12 +56,14 @@ module ActiveRecord
# table_exists?(:developers)
#
def table_exists?(table_name)
+ select_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present?
+ rescue NotImplementedError
tables.include?(table_name.to_s)
end
# Returns an array of view names defined in the database.
def views
- raise NotImplementedError, "#views is not implemented"
+ select_values(data_source_sql(type: "VIEW"), "SCHEMA")
end
# Checks to see if the view +view_name+ exists on the database.
@@ -65,11 +71,15 @@ module ActiveRecord
# view_exists?(:ebooks)
#
def view_exists?(view_name)
+ select_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present?
+ rescue NotImplementedError
views.include?(view_name.to_s)
end
# Returns an array of indexes for the given table.
- # def indexes(table_name, name = nil) end
+ def indexes(table_name, name = nil)
+ raise NotImplementedError, "#indexes is not implemented"
+ end
# Checks to see if an index exists on a table for a given index definition.
#
@@ -95,10 +105,12 @@ module ActiveRecord
indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
- # Returns an array of Column objects for the table specified by +table_name+.
- # See the concrete implementation for details on the expected parameter values.
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name)
- raise NotImplementedError, "#columns is not implemented"
+ table_name = table_name.to_s
+ column_definitions(table_name).map do |field|
+ new_column_from_field(table_name, field)
+ end
end
# Checks to see if a column exists in a given table.
@@ -120,7 +132,7 @@ module ActiveRecord
checks = []
checks << lambda { |c| c.name == column_name }
checks << lambda { |c| c.type == type } if type
- migration_keys.each do |attr|
+ column_options_keys.each do |attr|
checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr)
end
@@ -271,8 +283,8 @@ module ActiveRecord
yield td if block_given?
- if options[:force] && data_source_exists?(table_name)
- drop_table(table_name, options)
+ if options[:force]
+ drop_table(table_name, **options, if_exists: true)
end
result = execute schema_creation.accept td
@@ -332,18 +344,16 @@ module ActiveRecord
# part_id int NOT NULL,
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
- def create_join_table(table_1, table_2, options = {})
+ def create_join_table(table_1, table_2, column_options: {}, **options)
join_table_name = find_join_table_name(table_1, table_2, options)
- column_options = options.delete(:column_options) || {}
- column_options.reverse_merge!(null: false)
- type = column_options.delete(:type) || :integer
+ column_options.reverse_merge!(null: false, index: false)
- t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key }
+ t1_ref, t2_ref = [table_1, table_2].map { |t| t.to_s.singularize }
create_table(join_table_name, options.merge!(id: false)) do |td|
- td.send type, t1_column, column_options
- td.send type, t2_column, column_options
+ td.references t1_ref, column_options
+ td.references t2_ref, column_options
yield td if block_given?
end
end
@@ -766,16 +776,17 @@ module ActiveRecord
raise ArgumentError, "You must specify the index name"
end
else
- index_name(table_name, column: options)
+ index_name(table_name, index_name_options(options))
end
end
# Verifies the existence of an index with a given name.
- #
- # The default argument is returned if the underlying implementation does not define the indexes method,
- # as there's no way to determine the correct answer in that case.
- def index_name_exists?(table_name, index_name, default)
- return default unless respond_to?(:indexes)
+ 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
index_name = index_name.to_s
indexes(table_name).detect { |i| i.name == index_name }
end
@@ -826,8 +837,8 @@ module ActiveRecord
#
# add_reference(:products, :supplier, foreign_key: {to_table: :firms})
#
- def add_reference(table_name, *args)
- ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self))
+ def add_reference(table_name, ref_name, **options)
+ ReferenceDefinition.new(ref_name, options).add_to(update_table_definition(table_name, self))
end
alias :add_belongs_to :add_reference
@@ -854,6 +865,7 @@ module ActiveRecord
else
foreign_key_options = { to_table: reference_name }
end
+ foreign_key_options[:column] ||= "#{ref_name}_id"
remove_foreign_key(table_name, **foreign_key_options)
end
@@ -989,34 +1001,32 @@ module ActiveRecord
end
def dump_schema_information #:nodoc:
- versions = ActiveRecord::SchemaMigration.order("version").pluck(:version)
+ versions = ActiveRecord::SchemaMigration.all_versions
insert_versions_sql(versions)
end
def insert_versions_sql(versions) # :nodoc:
- sm_table = ActiveRecord::Migrator.schema_migrations_table_name
+ sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
- if supports_multi_insert?
+ if versions.is_a?(Array)
sql = "INSERT INTO #{sm_table} (version) VALUES\n"
- sql << versions.map { |v| "('#{v}')" }.join(",\n")
+ sql << versions.map { |v| "(#{quote(v)})" }.join(",\n")
sql << ";\n\n"
sql
else
- versions.map { |version|
- "INSERT INTO #{sm_table} (version) VALUES ('#{version}');"
- }.join "\n\n"
+ "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});"
end
end
- # Should not be called normally, but this operation is non-destructive.
- # The migrations module handles this automatically.
- def initialize_schema_migrations_table
+ def initialize_schema_migrations_table # :nodoc:
ActiveRecord::SchemaMigration.create_table
end
+ deprecate :initialize_schema_migrations_table
- def initialize_internal_metadata_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 }
@@ -1025,16 +1035,15 @@ module ActiveRecord
def assume_migrated_upto_version(version, migrations_paths)
migrations_paths = Array(migrations_paths)
version = version.to_i
- sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
+ sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
- migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
- paths = migrations_paths.map { |p| "#{p}/[0-9]*_*.rb" }
- versions = Dir[*paths].map do |filename|
- filename.split("/").last.split("_").first.to_i
+ 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
end
unless migrated.include?(version)
- execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
+ execute "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})"
end
inserting = (versions - migrated).select { |v| v < version }
@@ -1042,11 +1051,17 @@ module ActiveRecord
if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
end
- execute insert_versions_sql(inserting)
+ if supports_multi_insert?
+ execute insert_versions_sql(inserting)
+ else
+ inserting.each do |v|
+ execute insert_versions_sql(v)
+ end
+ end
end
end
- def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc:
type = type.to_sym if type
if native = native_database_types[type]
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
@@ -1064,7 +1079,7 @@ module ActiveRecord
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified"
end
- elsif [:datetime, :time].include?(type) && precision ||= native[:precision]
+ elsif [:datetime, :timestamp, :time, :interval].include?(type) && precision ||= native[:precision]
if (0..6) === precision
column_type_sql << "(#{precision})"
else
@@ -1116,18 +1131,14 @@ module ActiveRecord
end
def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
- if column_name.is_a?(String) && /\W/.match?(column_name)
- column_names = column_name
- else
- column_names = Array(column_name)
- end
+ column_names = index_column_names(column_name)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
index_name = options[:name].to_s if options.key?(:name)
- index_name ||= index_name(table_name, index_name_options(column_names))
+ index_name ||= index_name(table_name, column_names)
if options.key?(:algorithm)
algorithm = index_algorithms.fetch(options[:algorithm]) {
@@ -1143,7 +1154,7 @@ module ActiveRecord
validate_index_length!(table_name, index_name, options.fetch(:internal, false))
- if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ if data_source_exists?(table_name) && index_name_exists?(table_name, index_name)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
@@ -1165,12 +1176,16 @@ module ActiveRecord
raise NotImplementedError, "#{self.class} does not support changing column comments"
end
- protected
+ private
+ def column_options_keys
+ [:limit, :precision, :scale, :default, :null, :collation, :comment]
+ 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? }
@@ -1203,13 +1218,13 @@ module ActiveRecord
if options.is_a?(Hash)
checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name)
- column_names = Array(options[:column]).map(&:to_s)
+ column_names = index_column_names(options[:column])
else
- column_names = Array(options).map(&:to_s)
+ column_names = index_column_names(options)
end
- if column_names.any?
- checks << lambda { |i| i.columns.join("_and_") == column_names.join("_and_") }
+ if column_names.present?
+ checks << lambda { |i| index_name(table_name, i.columns) == index_name(table_name, column_names) }
end
raise ArgumentError, "No name or columns specified" if checks.none?
@@ -1248,7 +1263,6 @@ module ActiveRecord
end
end
- private
def create_table_definition(*args)
TableDefinition.new(*args)
end
@@ -1257,15 +1271,23 @@ module ActiveRecord
AlterTable.new create_table_definition(name)
end
- def index_name_options(column_names) # :nodoc:
- if column_names.is_a?(String)
+ def index_column_names(column_names)
+ if column_names.is_a?(String) && /\W/.match?(column_names)
+ column_names
+ else
+ Array(column_names)
+ end
+ end
+
+ def index_name_options(column_names)
+ if column_names.is_a?(String) && /\W/.match?(column_names)
column_names = column_names.scan(/\w+/).join("_")
end
{ column: column_names }
end
- def foreign_key_name(table_name, options) # :nodoc:
+ def foreign_key_name(table_name, options)
identifier = "#{table_name}_#{options.fetch(:column)}_fk"
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
options.fetch(:name) do
@@ -1273,7 +1295,7 @@ module ActiveRecord
end
end
- def validate_index_length!(table_name, new_name, internal = false) # :nodoc:
+ def validate_index_length!(table_name, new_name, internal = false)
max_index_length = internal ? index_name_length : allowed_index_name_length
if new_name.length > max_index_length
@@ -1292,6 +1314,14 @@ module ActiveRecord
def can_remove_index_by_name?(options)
options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
end
+
+ def data_source_sql(name = nil, type: nil)
+ raise NotImplementedError
+ end
+
+ def quoted_scope(name = nil, type: nil)
+ raise NotImplementedError
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 237367c8b3..550b4cc86b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -107,6 +107,7 @@ module ActiveRecord
@schema_cache = SchemaCache.new self
@quoted_column_names, @quoted_table_names = {}, {}
@visitor = arel_visitor
+ @lock = Monitor.new
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
@@ -153,22 +154,14 @@ module ActiveRecord
Arel::Visitors::ToSql.new(self)
end
- def valid_type?(type)
- false
+ def valid_type?(type) # :nodoc:
+ !native_database_types[type].nil?
end
def schema_creation
SchemaCreation.new self
end
- # Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name) # :nodoc:
- table_name = table_name.to_s
- column_definitions(table_name).map do |field|
- new_column_from_field(table_name, field)
- end
- end
-
# this method must only be called while holding connection pool's mutex
def lease
if in_use?
@@ -176,7 +169,7 @@ module ActiveRecord
if @owner == Thread.current
msg << "it is already leased by the current thread."
else
- msg << "it is already in use by a different thread: #{@owner}. " <<
+ msg << "it is already in use by a different thread: #{@owner}. " \
"Current thread: #{Thread.current}."
end
raise ActiveRecordError, msg
@@ -194,8 +187,8 @@ module ActiveRecord
def expire
if in_use?
if @owner != Thread.current
- raise ActiveRecordError, "Cannot expire connection, " <<
- "it is owned by a different thread: #{@owner}. " <<
+ raise ActiveRecordError, "Cannot expire connection, " \
+ "it is owned by a different thread: #{@owner}. " \
"Current thread: #{Thread.current}."
end
@@ -231,16 +224,15 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
- # Does this adapter support migrations?
- def supports_migrations?
- false
+ def supports_migrations? # :nodoc:
+ true
end
+ deprecate :supports_migrations?
- # Can this adapter determine the primary key for tables not attached
- # to an Active Record class, such as join tables?
- def supports_primary_key?
- false
+ 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?
@@ -310,6 +302,12 @@ module ActiveRecord
false
end
+ # Does this adapter support creating foreign key constraints
+ # in the same statement as creating the table?
+ def supports_foreign_keys_in_create?
+ supports_foreign_keys?
+ end
+
# Does this adapter support views?
def supports_views?
false
@@ -340,6 +338,11 @@ module ActiveRecord
true
end
+ # Does this adapter support virtual columns?
+ def supports_virtual_columns?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -428,12 +431,15 @@ module ActiveRecord
# 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
reconnect! unless active?
end
# Provides access to the underlying database driver for this adapter. For
# example, this method returns a Mysql2::Client object in case of Mysql2Adapter,
- # and a PGconn object in case of PostgreSQLAdapter.
+ # and a PG::Connection object in case of PostgreSQLAdapter.
#
# This is useful for when you need to call a proprietary method such as
# PostgreSQL's lo_* methods.
@@ -441,15 +447,15 @@ module ActiveRecord
@connection
end
- def case_sensitive_comparison(table, attribute, column, value)
- table[attribute].eq(Arel::Nodes::BindParam.new)
+ def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
+ table[attribute].eq(value)
end
- def case_insensitive_comparison(table, attribute, column, value)
+ def case_insensitive_comparison(table, attribute, column, value) # :nodoc:
if can_perform_case_insensitive_comparison_for?(column)
- table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new))
+ table[attribute].lower.eq(table.lower(value))
else
- table[attribute].eq(Arel::Nodes::BindParam.new)
+ table[attribute].eq(value)
end
end
@@ -499,9 +505,13 @@ module ActiveRecord
result
end
- protected
+ def default_index_type?(index) # :nodoc:
+ index.using.nil?
+ end
- def initialize_type_map(m) # :nodoc:
+ private
+
+ def initialize_type_map(m)
register_class_with_limit m, %r(boolean)i, Type::Boolean
register_class_with_limit m, %r(char)i, Type::String
register_class_with_limit m, %r(binary)i, Type::Binary
@@ -532,37 +542,37 @@ module ActiveRecord
end
end
- def reload_type_map # :nodoc:
+ def reload_type_map
type_map.clear
initialize_type_map(type_map)
end
- def register_class_with_limit(mapping, key, klass) # :nodoc:
+ def register_class_with_limit(mapping, key, klass)
mapping.register_type(key) do |*args|
limit = extract_limit(args.last)
klass.new(limit: limit)
end
end
- def register_class_with_precision(mapping, key, klass) # :nodoc:
+ def register_class_with_precision(mapping, key, klass)
mapping.register_type(key) do |*args|
precision = extract_precision(args.last)
klass.new(precision: precision)
end
end
- def extract_scale(sql_type) # :nodoc:
+ def extract_scale(sql_type)
case sql_type
when /\((\d+)\)/ then 0
when /\((\d+)(,(\d+))\)/ then $3.to_i
end
end
- def extract_precision(sql_type) # :nodoc:
+ def extract_precision(sql_type)
$1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
end
- def extract_limit(sql_type) # :nodoc:
+ def extract_limit(sql_type)
case sql_type
when /^bigint/i
8
@@ -583,7 +593,7 @@ module ActiveRecord
exception
end
- def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil)
+ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc:
@instrumenter.instrument(
"sql.active_record",
sql: sql,
@@ -591,21 +601,30 @@ module ActiveRecord
binds: binds,
type_casted_binds: type_casted_binds,
statement_name: statement_name,
- connection_id: object_id) { yield }
+ connection_id: object_id) do
+ @lock.synchronize do
+ yield
+ end
+ end
rescue => e
raise translate_exception_class(e, sql)
end
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ case exception
+ when RuntimeError
+ exception
+ else
+ ActiveRecord::StatementInvalid.new(message)
+ end
end
def without_prepared_statement?(binds)
!prepared_statements || binds.empty?
end
- def column_for(table_name, column_name) # :nodoc:
+ def column_for(table_name, column_name)
column_name = column_name.to_s
columns(table_name).detect { |c| c.name == column_name } ||
raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
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 cbbba5b1a5..55ec112c17 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -6,6 +6,7 @@ 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"
require "active_support/core_ext/string/strip"
@@ -15,6 +16,7 @@ module ActiveRecord
class AbstractMysqlAdapter < AbstractAdapter
include MySQL::Quoting
include MySQL::ColumnDumper
+ include MySQL::SchemaStatements
def update_table_definition(table_name, base) # :nodoc:
MySQL::Table.new(table_name, base)
@@ -39,13 +41,14 @@ module ActiveRecord
self.emulate_booleans = true
NATIVE_DATABASE_TYPES = {
- primary_key: "int auto_increment PRIMARY KEY",
+ primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
text: { name: "text", limit: 65535 },
integer: { name: "int", limit: 4 },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
+ timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob", limit: 65535 },
@@ -67,8 +70,8 @@ module ActiveRecord
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
- if version < "5.0.0"
- raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0."
+ if version < "5.1.10"
+ raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10."
end
end
@@ -88,15 +91,6 @@ module ActiveRecord
/mariadb/i.match?(full_version)
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
- true
- end
-
- def supports_primary_key?
- true
- end
-
def supports_bulk_alter? #:nodoc:
true
end
@@ -141,6 +135,14 @@ module ActiveRecord
end
end
+ def supports_virtual_columns?
+ if mariadb?
+ version >= "5.2.0"
+ else
+ version >= "5.7.5"
+ end
+ end
+
def supports_advisory_locks?
true
end
@@ -310,72 +312,18 @@ module ActiveRecord
show_variable "collation_database"
end
- def tables(name = nil) # :nodoc:
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #tables currently returns both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
- Use #data_sources instead.
- MSG
-
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing arguments to #tables is deprecated without replacement.
- MSG
- end
-
- data_sources
- end
-
- def data_sources
- sql = "SELECT table_name FROM information_schema.tables "
- sql << "WHERE table_schema = #{quote(@config[:database])}"
-
- select_values(sql, "SCHEMA")
- end
-
def truncate(table_name, name = nil)
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
end
- def table_exists?(table_name)
- # Update lib/active_record/internal_metadata.rb when this gets removed
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #table_exists? currently checks both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
- Use #data_source_exists? instead.
- MSG
-
- data_source_exists?(table_name)
- end
-
- def data_source_exists?(table_name)
- return false unless table_name.present?
-
- schema, name = extract_schema_qualified_name(table_name)
-
- sql = "SELECT table_name FROM information_schema.tables "
- sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
-
- select_values(sql, "SCHEMA").any?
- end
-
- def views # :nodoc:
- select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA")
- end
-
- def view_exists?(view_name) # :nodoc:
- return false unless view_name.present?
-
- schema, name = extract_schema_qualified_name(view_name)
-
- sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
- sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
-
- select_values(sql, "SCHEMA").any?
- 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
+
indexes = []
current_index = nil
execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
@@ -409,13 +357,13 @@ module ActiveRecord
end
def table_comment(table_name) # :nodoc:
- schema, name = extract_schema_qualified_name(table_name)
+ scope = quoted_scope(table_name)
select_value(<<-SQL.strip_heredoc, "SCHEMA")
SELECT table_comment
FROM information_schema.tables
- WHERE table_schema = #{quote(schema)}
- AND table_name = #{quote(name)}
+ WHERE table_schema = #{scope[:schema]}
+ AND table_name = #{scope[:name]}
SQL
end
@@ -515,7 +463,7 @@ module ActiveRecord
def foreign_keys(table_name)
raise ArgumentError unless table_name.present?
- schema, name = extract_schema_qualified_name(table_name)
+ scope = quoted_scope(table_name)
fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA")
SELECT fk.referenced_table_name AS 'to_table',
@@ -528,8 +476,9 @@ module ActiveRecord
JOIN information_schema.referential_constraints rc
USING (constraint_schema, constraint_name)
WHERE fk.referenced_column_name IS NOT NULL
- AND fk.table_schema = #{quote(schema)}
- AND fk.table_name = #{quote(name)}
+ AND fk.table_schema = #{scope[:schema]}
+ AND fk.table_name = #{scope[:name]}
+ AND rc.table_name = #{scope[:name]}
SQL
fk_info.map do |row|
@@ -568,7 +517,7 @@ module ActiveRecord
end
# Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc:
sql = \
case type.to_s
when "integer"
@@ -584,7 +533,7 @@ module ActiveRecord
binary_to_sql(limit)
end
else
- super(type, limit, precision, scale)
+ super
end
sql << " unsigned" if unsigned && type != :primary_key
@@ -601,21 +550,21 @@ module ActiveRecord
def primary_keys(table_name) # :nodoc:
raise ArgumentError unless table_name.present?
- schema, name = extract_schema_qualified_name(table_name)
+ scope = quoted_scope(table_name)
select_values(<<-SQL.strip_heredoc, "SCHEMA")
SELECT column_name
FROM information_schema.key_column_usage
WHERE constraint_name = 'PRIMARY'
- AND table_schema = #{quote(schema)}
- AND table_name = #{quote(name)}
+ AND table_schema = #{scope[:schema]}
+ AND table_name = #{scope[:name]}
ORDER BY ordinal_position
SQL
end
- def case_sensitive_comparison(table, attribute, column, value)
+ def case_sensitive_comparison(table, attribute, column, value) # :nodoc:
if column.collation && !column.case_sensitive?
- table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
+ table[attribute].eq(Arel::Nodes::Bin.new(value))
else
super
end
@@ -645,13 +594,13 @@ module ActiveRecord
self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
end
- def valid_type?(type)
- !native_database_types[type].nil?
+ def default_index_type?(index) # :nodoc:
+ index.using == :btree || super
end
- protected
+ private
- def initialize_type_map(m) # :nodoc:
+ def initialize_type_map(m)
super
register_class_with_limit m, %r(char)i, MysqlString
@@ -691,9 +640,9 @@ module ActiveRecord
end
end
- def register_integer_type(mapping, key, options) # :nodoc:
+ def register_integer_type(mapping, key, options)
mapping.register_type(key) do |sql_type|
- if /\bunsigned\z/.match?(sql_type)
+ if /\bunsigned\b/.match?(sql_type)
Type::UnsignedInteger.new(options)
else
Type::Integer.new(options)
@@ -702,7 +651,7 @@ module ActiveRecord
end
def extract_precision(sql_type)
- if /time/.match?(sql_type)
+ if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type)
super || 0
else
super
@@ -717,6 +666,7 @@ module ActiveRecord
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})" }
@@ -733,9 +683,14 @@ module ActiveRecord
# See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
ER_DUP_ENTRY = 1062
+ ER_NOT_NULL_VIOLATION = 1048
+ ER_DO_NOT_HAVE_DEFAULT = 1364
ER_NO_REFERENCED_ROW_2 = 1452
ER_DATA_TOO_LONG = 1406
+ ER_OUT_OF_RANGE = 1264
ER_LOCK_DEADLOCK = 1213
+ ER_CANNOT_ADD_FOREIGN = 1215
+ ER_CANNOT_CREATE_TABLE = 1005
def translate_exception(exception, message)
case error_number(exception)
@@ -743,8 +698,20 @@ module ActiveRecord
RecordNotUnique.new(message)
when ER_NO_REFERENCED_ROW_2
InvalidForeignKey.new(message)
+ when ER_CANNOT_ADD_FOREIGN
+ mismatched_foreign_key(message)
+ when ER_CANNOT_CREATE_TABLE
+ if message.include?("errno: 150")
+ mismatched_foreign_key(message)
+ else
+ super
+ end
when ER_DATA_TOO_LONG
ValueTooLong.new(message)
+ when ER_OUT_OF_RANGE
+ RangeError.new(message)
+ when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
+ NotNullViolation.new(message)
when ER_LOCK_DEADLOCK
Deadlocked.new(message)
else
@@ -761,14 +728,18 @@ module ActiveRecord
def change_column_sql(table_name, column_name, type, options = {})
column = column_for(table_name, column_name)
- unless options_include_default?(options)
+ unless options.key?(:default)
options[:default] = column.default
end
- unless options.has_key?(:null)
+ unless options.key?(:null)
options[:null] = column.null
end
+ unless options.key?(:comment)
+ options[:comment] = column.comment
+ end
+
td = create_table_definition(table_name)
cd = td.new_column_definition(column.name, type, options)
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
@@ -815,8 +786,6 @@ module ActiveRecord
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
end
- private
-
# MySQL is too stupid to create a temporary table for use subquery, so we have
# to give it some prompting in the form of a subsubquery. Ugh!
def subquery_for(key, select)
@@ -843,9 +812,9 @@ module ActiveRecord
variables["sql_auto_is_null"] = 0
# Increase timeout so the server doesn't disconnect us.
- wait_timeout = @config[:wait_timeout]
+ wait_timeout = self.class.type_cast_config_to_integer(@config[:wait_timeout])
wait_timeout = 2147483 unless wait_timeout.is_a?(Integer)
- variables["wait_timeout"] = self.class.type_cast_config_to_integer(wait_timeout)
+ variables["wait_timeout"] = wait_timeout
defaults = [":default", :default].to_set
@@ -886,7 +855,7 @@ module ActiveRecord
end.compact.join(", ")
# ...and send them all in one query
- @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
+ execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
end
def column_definitions(table_name) # :nodoc:
@@ -910,10 +879,16 @@ module ActiveRecord
MySQL::TableDefinition.new(*args)
end
- def extract_schema_qualified_name(string) # :nodoc:
- schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/)
- schema, name = @config[:database], schema unless name
- [schema, name]
+ def mismatched_foreign_key(message)
+ parts = message.scan(/`(\w+)`[ $)]/).flatten
+ MismatchedForeignKey.new(
+ self,
+ message: message,
+ table: parts[0],
+ foreign_key: parts[1],
+ target_table: parts[2],
+ primary_key: parts[3],
+ )
end
def integer_to_sql(limit) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 02d546209d..61cd7ae4cc 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -40,6 +40,28 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
+ def init_with(coder)
+ @name = coder["name"]
+ @table_name = coder["table_name"]
+ @sql_type_metadata = coder["sql_type_metadata"]
+ @null = coder["null"]
+ @default = coder["default"]
+ @default_function = coder["default_function"]
+ @collation = coder["collation"]
+ @comment = coder["comment"]
+ end
+
+ def encode_with(coder)
+ coder["name"] = @name
+ coder["table_name"] = @table_name
+ coder["sql_type_metadata"] = @sql_type_metadata
+ coder["null"] = @null
+ coder["default"] = @default
+ coder["default_function"] = @default_function
+ coder["collation"] = @collation
+ coder["comment"] = @comment
+ end
+
def ==(other)
other.is_a?(Column) &&
attributes_for_hash == other.attributes_for_hash
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index dcf56997db..3e4ea28f63 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -149,9 +149,18 @@ module ActiveRecord
# Expands each key in @configurations hash into fully resolved hash
def resolve_all
config = configurations.dup
+
+ if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ 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|
config[key] = resolve(value) if value
end
+
config
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
index f82c556a6f..c9ad47c035 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
@@ -5,7 +5,7 @@ module ActiveRecord
delegate :extra, to: :sql_type_metadata, allow_nil: true
def unsigned?
- /\bunsigned\z/.match?(sql_type)
+ /\bunsigned(?: zerofill)?\z/.match?(sql_type)
end
def case_sensitive?
@@ -15,6 +15,10 @@ module ActiveRecord
def auto_increment?
extra == "auto_increment"
end
+
+ def virtual?
+ /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
+ end
end
end
end
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 c7098105a8..8c67a7a80b 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module MySQL
module DatabaseStatements
# Returns an ActiveRecord::Result instance.
- def select_all(arel, name = nil, binds = [], preparable: nil)
+ def select_all(arel, name = nil, binds = [], preparable: nil) # :nodoc:
result = if ExplainRegistry.collect? && prepared_statements
unprepared_statement { super }
else
@@ -15,8 +15,8 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil, binds = [])
- select_result(sql, name, binds) do |result|
+ def select_rows(arel, name = nil, binds = []) # :nodoc:
+ select_result(arel, name, binds) do |result|
@connection.next_result while @connection.more_results?
result.to_a
end
@@ -52,15 +52,15 @@ module ActiveRecord
end
alias :exec_update :exec_delete
- protected
+ private
def last_inserted_id(result)
@connection.last_id
end
- private
-
- def select_result(sql, name = nil, binds = [])
+ def select_result(arel, name, binds)
+ arel, binds = binds_from_relation(arel, binds)
+ sql = to_sql(arel, binds)
if without_prepared_statement?(binds)
execute_and_free(sql, name) { |result| yield result }
else
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
index 9d11ad28d4..d4f5906b33 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -36,15 +36,9 @@ module ActiveRecord
end
end
- private
-
- def _quote(value)
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
- super
- end
- end
+ def quoted_binary(value)
+ "x'#{value.hex}'"
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index d808b50332..083cd6340f 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -1,9 +1,9 @@
module ActiveRecord
module ConnectionAdapters
module MySQL
- class SchemaCreation < AbstractAdapter::SchemaCreation
- delegate :add_sql_comment!, to: :@conn
- private :add_sql_comment!
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
+ delegate :add_sql_comment!, :mariadb?, to: :@conn
+ private :add_sql_comment!, :mariadb?
private
@@ -11,11 +11,6 @@ module ActiveRecord
"DROP FOREIGN KEY #{name}"
end
- def visit_ColumnDefinition(o)
- o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned)
- super
- end
-
def visit_AddColumnDefinition(o)
add_column_position!(super, column_options(o.column))
end
@@ -29,13 +24,15 @@ module ActiveRecord
add_sql_comment!(super, options[:comment])
end
- def column_options(o)
- column_options = super
- column_options[:charset] = o.charset
- column_options
- end
-
def add_column_options!(sql, options)
+ # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values,
+ # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP
+ # column to contain NULL, explicitly declare it with the NULL attribute.
+ # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html
+ if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key]
+ sql << " NULL" unless options[:null] == false || options_include_default?(options)
+ end
+
if charset = options[:charset]
sql << " CHARACTER SET #{charset}"
end
@@ -44,6 +41,13 @@ module ActiveRecord
sql << " COLLATE #{collation}"
end
+ if as = options[:as]
+ sql << " AS (#{as})"
+ if options[:stored]
+ sql << (mariadb? ? " PERSISTENT" : " STORED")
+ end
+ end
+
add_sql_comment!(super, options[:comment])
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index ce773ed75b..6d88c14d50 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module MySQL
module ColumnMethods
def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if type == :bigint && !options.key?(:default)
+ options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
super
end
@@ -56,32 +56,29 @@ module ActiveRecord
end
end
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset, :unsigned
- end
-
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
- def new_column_definition(name, type, options) # :nodoc:
- column = super
- case column.type
+ def new_column_definition(name, type, **options) # :nodoc:
+ case type
+ when :virtual
+ type = options[:type]
when :primary_key
- column.type = :integer
- column.auto_increment = true
+ type = :integer
+ options[:limit] ||= 8
+ options[:auto_increment] = true
+ options[:primary_key] = true
when /\Aunsigned_(?<type>.+)\z/
- column.type = $~[:type].to_sym
- column.unsigned = true
+ type = $~[:type].to_sym
+ options[:unsigned] = true
end
- column.unsigned ||= options[:unsigned]
- column.charset = options[:charset]
- column
+
+ super
end
private
-
- def create_column_definition(name, type)
- MySQL::ColumnDefinition.new(name, type)
+ def aliased_types(name, fallback)
+ fallback
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index 9b02d8a34b..3e0afd9761 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -1,21 +1,17 @@
module ActiveRecord
module ConnectionAdapters
module MySQL
- module ColumnDumper
- def column_spec_for_primary_key(column)
- if column.bigint?
- spec = { id: :bigint.inspect }
- spec[:default] = schema_default(column) || "nil" unless column.auto_increment?
- else
- spec = super
- end
- spec[:unsigned] = "true" if column.unsigned?
- spec
- end
-
+ module ColumnDumper # :nodoc:
def prepare_column_options(column)
spec = super
spec[:unsigned] = "true" if column.unsigned?
+
+ if supports_virtual_columns? && column.virtual?
+ spec[:as] = extract_expression_for_virtual_column(column)
+ spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
+ spec = { type: schema_type(column).inspect }.merge!(spec)
+ end
+
spec
end
@@ -26,11 +22,18 @@ module ActiveRecord
private
def default_primary_key?(column)
- super && column.auto_increment?
+ super && column.auto_increment? && !column.unsigned?
+ end
+
+ def explicit_primary_key_default?(column)
+ column.type == :integer && !column.auto_increment?
end
def schema_type(column)
- if column.sql_type == "tinyblob"
+ case column.sql_type
+ when /\Atimestamp\b/
+ :timestamp
+ when "tinyblob"
:blob
else
super
@@ -38,7 +41,7 @@ module ActiveRecord
end
def schema_precision(column)
- super unless /time/.match?(column.sql_type) && column.precision == 0
+ super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
def schema_collation(column)
@@ -48,6 +51,21 @@ module ActiveRecord
column.collation.inspect if column.collation != @table_collation_cache[table_name]
end
end
+
+ def extract_expression_for_virtual_column(column)
+ if mariadb?
+ create_table_info = create_table_info(column.table_name)
+ if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)} AS \((?<expression>.+?)\) #{column.extra}/m =~ create_table_info
+ $~[:expression].inspect
+ end
+ else
+ sql = "SELECT generation_expression FROM information_schema.columns" \
+ " WHERE table_schema = #{quote(@config[:database])}" \
+ " AND table_name = #{quote(column.table_name)}" \
+ " AND column_name = #{quote(column.name)}"
+ select_value(sql, "SCHEMA").inspect
+ end
+ end
end
end
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
new file mode 100644
index 0000000000..10c8bd179a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -0,0 +1,33 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module SchemaStatements # :nodoc:
+ private
+ def data_source_sql(name = nil, type: nil)
+ scope = quoted_scope(name, type: type)
+
+ sql = "SELECT table_name FROM information_schema.tables"
+ sql << " WHERE table_schema = #{scope[:schema]}"
+ sql << " AND table_name = #{scope[:name]}" if scope[:name]
+ sql << " AND table_type = #{scope[:type]}" if scope[:type]
+ sql
+ end
+
+ def quoted_scope(name = nil, type: nil)
+ schema, name = extract_schema_qualified_name(name)
+ scope = {}
+ scope[:schema] = schema ? quote(schema) : "database()"
+ scope[:name] = quote(name) if name
+ scope[:type] = quote(type) if type
+ scope
+ end
+
+ def extract_schema_qualified_name(string)
+ schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/)
+ schema, name = nil, schema unless name
+ [schema, name]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
index 24dcf852e1..9ad6a6c0d0 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -2,6 +2,8 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
+ undef to_yaml if method_defined?(:to_yaml)
+
attr_reader :extra
def initialize(type_metadata, extra: "")
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 520a50506f..705e6063dc 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -7,18 +7,14 @@ module ActiveRecord
PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
end
- def select_value(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- execute_and_clear(sql, name, binds) do |result|
+ def select_value(arel, name = nil, binds = []) # :nodoc:
+ select_result(arel, name, binds) do |result|
result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
end
end
- def select_values(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- execute_and_clear(sql, name, binds) do |result|
+ def select_values(arel, name = nil, binds = []) # :nodoc:
+ select_result(arel, name, binds) do |result|
if result.nfields > 0
result.column_values(0)
else
@@ -29,8 +25,8 @@ module ActiveRecord
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
- def select_rows(sql, name = nil, binds = [])
- execute_and_clear(sql, name, binds) do |result|
+ def select_rows(arel, name = nil, binds = []) # :nodoc:
+ select_result(arel, name, binds) do |result|
result.values
end
end
@@ -134,7 +130,7 @@ module ActiveRecord
super
end
- protected :sql_for_insert
+ private :sql_for_insert
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
if use_insert_returning? || pk == false
@@ -179,6 +175,14 @@ module ActiveRecord
def suppress_composite_primary_key(pk)
pk unless pk.is_a?(Array)
end
+
+ def select_result(arel, name, binds)
+ arel, binds = binds_from_relation(arel, binds)
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ yield result
+ end
+ end
end
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 0e526f6201..4098250f3e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -11,6 +11,7 @@ require "active_record/connection_adapters/postgresql/oid/inet"
require "active_record/connection_adapters/postgresql/oid/json"
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"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index d9daaaa23e..a73a8c1726 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -5,8 +5,10 @@ module ActiveRecord
class Array < Type::Value # :nodoc:
include Type::Helpers::Mutable
+ Data = Struct.new(:encoder, :values) # :nodoc:
+
attr_reader :subtype, :delimiter
- delegate :type, :user_input_in_time_zone, :limit, to: :subtype
+ delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype
def initialize(subtype, delimiter = ",")
@subtype = subtype
@@ -17,8 +19,11 @@ module ActiveRecord
end
def deserialize(value)
- if value.is_a?(::String)
+ case value
+ when ::String
type_cast_array(@pg_decoder.decode(value), :deserialize)
+ when Data
+ type_cast_array(value.values, :deserialize)
else
super
end
@@ -33,11 +38,8 @@ module ActiveRecord
def serialize(value)
if value.is_a?(::Array)
- result = @pg_encoder.encode(type_cast_array(value, :serialize))
- if encoding = determine_encoding_of_strings(value)
- result.force_encoding(encoding)
- end
- result
+ casted_values = type_cast_array(value, :serialize)
+ Data.new(@pg_encoder, casted_values)
else
super
end
@@ -58,6 +60,10 @@ module ActiveRecord
value.map(&block)
end
+ def changed_in_place?(raw_old_value, new_value)
+ deserialize(raw_old_value) != new_value
+ end
+
private
def type_cast_array(value, method)
@@ -67,13 +73,6 @@ module ActiveRecord
@subtype.public_send(method, value)
end
end
-
- def determine_encoding_of_strings(value)
- case value
- when ::Array then determine_encoding_of_strings(value.first)
- when ::String then value.encoding
- end
- end
end
end
end
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 302d393277..0a505f46a7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -41,6 +41,8 @@ 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
attr_reader :value
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
index 8f9d6e7f9b..702fa8175c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def deserialize(value)
return if value.nil?
return value.to_s if value.is_a?(Type::Binary::Data)
- PGconn.unescape_bytea(super)
+ PG::Connection.unescape_bytea(super)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index d629ebca91..49dd4fc73f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -35,6 +35,14 @@ module ActiveRecord
ActiveRecord::Store::StringKeyedHashAccessor
end
+ # Will compare the Hash equivalents of +raw_old_value+ and +new_value+.
+ # By comparing hashes, this avoids an edge case where the order of
+ # the keys change between the two hashes, and they would not be marked
+ # as equal.
+ def changed_in_place?(raw_old_value, new_value)
+ deserialize(raw_old_value) != new_value
+ end
+
private
HstorePair = begin
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb
new file mode 100644
index 0000000000..9c2ac08b30
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Oid < Type::Integer # :nodoc:
+ def type
+ :oid
+ end
+ end
+ end
+ 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 2c714f4018..54d5d0902e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -1,5 +1,3 @@
-require "active_support/core_ext/string/filters"
-
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
index 2d2fede4e8..564e82a4ac 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -5,8 +5,9 @@ module ActiveRecord
class SpecializedString < Type::String # :nodoc:
attr_reader :type
- def initialize(type)
+ def initialize(type, **options)
@type = type
+ super(options)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index b5031d890f..e399b6e499 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -33,7 +33,7 @@ module ActiveRecord
# Quotes schema names for use in SQL queries.
def quote_schema_name(name)
- PGconn.quote_ident(name)
+ PG::Connection.quote_ident(name)
end
def quote_table_name_for_assignment(table, attr)
@@ -42,7 +42,7 @@ module ActiveRecord
# Quotes column names for use in SQL queries.
def quote_column_name(name) # :nodoc:
- @quoted_column_names[name] ||= PGconn.quote_ident(super).freeze
+ @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
end
# Quote date/time values for use in SQL input.
@@ -55,6 +55,10 @@ module ActiveRecord
end
end
+ def quoted_binary(value) # :nodoc:
+ "'#{escape_bytea(value.to_s)}'"
+ end
+
def quote_default_expression(value, column) # :nodoc:
if value.is_a?(Proc)
value.call
@@ -76,8 +80,6 @@ module ActiveRecord
def _quote(value)
case value
- when Type::Binary::Data
- "'#{escape_bytea(value.to_s)}'"
when OID::Xml::Data
"xml '#{quote_string(value.to_s)}'"
when OID::Bit::Data
@@ -92,6 +94,8 @@ module ActiveRecord
else
super
end
+ when OID::Array::Data
+ _quote(encode_array(value))
else
super
end
@@ -101,15 +105,42 @@ module ActiveRecord
case value
when Type::Binary::Data
# Return a bind param hash with format as binary.
- # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc
# for more information
{ value: value.to_s, format: 1 }
when OID::Xml::Data, OID::Bit::Data
value.to_s
+ when OID::Array::Data
+ encode_array(value)
else
super
end
end
+
+ def encode_array(array_data)
+ encoder = array_data.encoder
+ values = type_cast_array(array_data.values)
+
+ result = encoder.encode(values)
+ if encoding = determine_encoding_of_strings_in_array(values)
+ result.force_encoding(encoding)
+ end
+ result
+ end
+
+ def determine_encoding_of_strings_in_array(value)
+ case value
+ when ::Array then determine_encoding_of_strings_in_array(value.first)
+ when ::String then value.encoding
+ end
+ end
+
+ def type_cast_array(values)
+ case values
+ when ::Array then values.map { |item| type_cast_array(item) }
+ else _type_cast(values)
+ end
+ 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 44a7338bf5..730e7c7137 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -6,8 +6,50 @@ module ActiveRecord
true
end
- def disable_referential_integrity # :nodoc:
+ def disable_referential_integrity(&block) # :nodoc:
if supports_disable_referential_integrity?
+ if supports_alter_constraint?
+ disable_referential_integrity_with_alter_constraint(&block)
+ else
+ disable_referential_integrity_with_disable_trigger(&block)
+ end
+ else
+ yield
+ end
+ end
+
+ private
+
+ def disable_referential_integrity_with_alter_constraint
+ tables_constraints = execute(<<-SQL).values
+ SELECT table_name, constraint_name
+ FROM information_schema.table_constraints
+ WHERE constraint_type = 'FOREIGN KEY'
+ AND is_deferrable = 'NO'
+ SQL
+
+ execute(
+ tables_constraints.collect { |table, constraint|
+ "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{constraint} DEFERRABLE"
+ }.join(";")
+ )
+
+ begin
+ transaction do
+ execute("SET CONSTRAINTS ALL DEFERRED")
+
+ yield
+ end
+ ensure
+ execute(
+ tables_constraints.collect { |table, constraint|
+ "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{constraint} NOT DEFERRABLE"
+ }.join(";")
+ )
+ end
+ end
+
+ def disable_referential_integrity_with_disable_trigger
original_exception = nil
begin
@@ -39,10 +81,7 @@ Rails needs superuser privileges to disable referential integrity.
end
rescue ActiveRecord::ActiveRecordError
end
- else
- yield
end
- end
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
new file mode 100644
index 0000000000..e1d5089115
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
+ private
+ def add_column_options!(sql, options)
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ super
+ end
+ end
+ end
+ end
+end
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 a11dbe7dce..11ea1e5144 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -11,11 +11,22 @@ module ActiveRecord
# t.timestamps
# end
#
- # By default, this will use the +uuid_generate_v4()+ function from the
- # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
- # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
- # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
- # set the +:default+ option to +nil+:
+ # By default, this will use the +gen_random_uuid()+ function from the
+ # +pgcrypto+ extension. As that extension is only available in
+ # PostgreSQL 9.4+, for earlier versions an explicit default can be set
+ # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: "uuid_generate_v4()"
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # To enable the appropriate extension, which is a requirement, use
+ # the +enable_extension+ method in your migrations.
+ #
+ # To use a UUID primary key without any of the extensions, set the
+ # +:default+ option to +nil+:
#
# create_table :stuffs, id: false do |t|
# t.primary_key :id, :uuid, default: nil
@@ -23,15 +34,25 @@ module ActiveRecord
# t.timestamps
# end
#
- # You may also pass a different UUID generation function from +uuid-ossp+
- # or another library.
+ # You may also pass a custom stored procedure that returns a UUID or use a
+ # different UUID generation function from another library.
#
# Note that setting the UUID primary key default value to +nil+ will
# require you to assure that you always provide a UUID value before saving
# 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[:default] = options.fetch(:default, "uuid_generate_v4()") if type == :uuid
+ 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
end
@@ -67,6 +88,10 @@ module ActiveRecord
args.each { |name| column(name, :inet, options) }
end
+ def interval(*args, **options)
+ args.each { |name| column(name, :interval, options) }
+ end
+
def int4range(*args, **options)
args.each { |name| column(name, :int4range, options) }
end
@@ -99,6 +124,10 @@ module ActiveRecord
args.each { |name| column(name, :numrange, options) }
end
+ def oid(*args, **options)
+ args.each { |name| column(name, :oid, options) }
+ end
+
def point(*args, **options)
args.each { |name| column(name, :point, options) }
end
@@ -152,24 +181,8 @@ module ActiveRecord
end
end
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :array
- end
-
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
-
- def new_column_definition(name, type, options) # :nodoc:
- column = super
- column.array = options[:array]
- column
- end
-
- private
-
- def create_column_definition(name, type)
- PostgreSQL::ColumnDefinition.new name, type
- end
end
class Table < ActiveRecord::ConnectionAdapters::Table
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 c20baf655c..5555a46b6b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -1,15 +1,7 @@
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
- module ColumnDumper
- def column_spec_for_primary_key(column)
- spec = super
- if schema_type(column) == :uuid
- spec[:default] ||= "nil"
- end
- spec
- end
-
+ module ColumnDumper # :nodoc:
# Adds +:array+ option to the default set
def prepare_column_options(column)
spec = super
@@ -25,7 +17,11 @@ module ActiveRecord
private
def default_primary_key?(column)
- schema_type(column) == :serial
+ schema_type(column) == :bigserial
+ end
+
+ def explicit_primary_key_default?(column)
+ column.type == :uuid || (column.type == :integer && !column.serial?)
end
def schema_type(column)
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 9e7487b27f..a332375b78 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -3,22 +3,6 @@ require "active_support/core_ext/string/strip"
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
- class SchemaCreation < AbstractAdapter::SchemaCreation
- private
-
- def visit_ColumnDefinition(o)
- o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array)
- super
- end
-
- def add_column_options!(sql, options)
- if options[:collation]
- sql << " COLLATE \"#{options[:collation]}\""
- end
- super
- end
- end
-
module SchemaStatements
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
@@ -70,91 +54,24 @@ module ActiveRecord
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
- # Returns the list of all tables in the schema search path.
- def tables(name = nil)
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing arguments to #tables is deprecated without replacement.
- MSG
- end
-
- select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", "SCHEMA")
- end
-
- def data_sources # :nodoc
- select_values(<<-SQL, "SCHEMA")
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
- AND n.nspname = ANY (current_schemas(false))
- SQL
- end
-
- # Returns true if table exists.
- # If the schema is not specified as part of +name+ then it will only find tables within
- # the current schema search path (regardless of permissions to access tables in other schemas)
- def table_exists?(name)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #table_exists? currently checks both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
- Use #data_source_exists? instead.
- MSG
-
- data_source_exists?(name)
- end
-
- def data_source_exists?(name)
- name = Utils.extract_schema_qualified_name(name.to_s)
- return false unless name.identifier
-
- select_values(<<-SQL, "SCHEMA").any?
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
- AND c.relname = #{quote(name.identifier)}
- AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"}
- SQL
- end
-
- def views # :nodoc:
- select_values(<<-SQL, "SCHEMA")
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
- AND n.nspname = ANY (current_schemas(false))
- SQL
- end
-
- def view_exists?(view_name) # :nodoc:
- name = Utils.extract_schema_qualified_name(view_name.to_s)
- return false unless name.identifier
-
- select_values(<<-SQL, "SCHEMA").any?
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
- AND c.relname = #{quote(name.identifier)}
- AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"}
- SQL
- end
-
def drop_table(table_name, options = {}) # :nodoc:
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
# Returns true if schema exists.
def schema_exists?(name)
- select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", "SCHEMA").to_i > 0
+ select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0
end
# Verifies existence of an index with a given name.
- def index_name_exists?(table_name, index_name, default)
- table = Utils.extract_schema_qualified_name(table_name.to_s)
- index = Utils.extract_schema_qualified_name(index_name.to_s)
+ 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
+ table = quoted_scope(table_name)
+ index = quoted_scope(index_name)
select_value(<<-SQL, "SCHEMA").to_i > 0
SELECT COUNT(*)
@@ -163,15 +80,21 @@ module ActiveRecord
INNER JOIN pg_class i ON d.indexrelid = i.oid
LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
WHERE i.relkind = 'i'
- AND i.relname = '#{index.identifier}'
- AND t.relname = '#{table.identifier}'
- AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'}
+ AND i.relname = #{index[:name]}
+ AND t.relname = #{table[:name]}
+ AND n.nspname = #{index[:schema]}
SQL
end
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- table = Utils.extract_schema_qualified_name(table_name.to_s)
+ def indexes(table_name, name = nil) # :nodoc:
+ if name
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing name to #indexes is deprecated without replacement.
+ MSG
+ end
+
+ scope = quoted_scope(table_name)
result = query(<<-SQL, "SCHEMA")
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
@@ -185,8 +108,8 @@ module ActiveRecord
LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
- AND t.relname = '#{table.identifier}'
- AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'}
+ AND t.relname = #{scope[:name]}
+ AND n.nspname = #{scope[:schema]}
ORDER BY i.relname
SQL
@@ -248,22 +171,22 @@ module ActiveRecord
# Returns a comment stored in database for given table
def table_comment(table_name) # :nodoc:
- name = Utils.extract_schema_qualified_name(table_name.to_s)
- if name.identifier
+ scope = quoted_scope(table_name, type: "BASE TABLE")
+ if scope[:name]
select_value(<<-SQL.strip_heredoc, "SCHEMA")
SELECT pg_catalog.obj_description(c.oid, 'pg_class')
FROM pg_catalog.pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relname = #{quote(name.identifier)}
- AND c.relkind IN ('r') -- (r)elation/table
- AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'}
+ WHERE c.relname = #{scope[:name]}
+ AND c.relkind IN (#{scope[:type]})
+ AND n.nspname = #{scope[:schema]}
SQL
end
end
# Returns the current database name.
def current_database
- select_value("select current_database()", "SCHEMA")
+ select_value("SELECT current_database()", "SCHEMA")
end
# Returns the current schema name.
@@ -439,17 +362,16 @@ module ActiveRecord
end
def primary_keys(table_name) # :nodoc:
+ scope = quoted_scope(table_name)
select_values(<<-SQL.strip_heredoc, "SCHEMA")
- WITH pk_constraint AS (
- SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint
- WHERE contype = 'p'
- AND conrelid = #{quote(quote_table_name(table_name))}::regclass
- ), cons AS (
- SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint
- )
- SELECT attr.attname FROM pg_attribute attr
- INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum
- ORDER BY cons.rownum
+ SELECT column_name
+ FROM information_schema.key_column_usage kcu
+ JOIN information_schema.table_constraints tc
+ USING (table_schema, table_name, constraint_name)
+ WHERE constraint_type = 'PRIMARY KEY'
+ AND kcu.table_name = #{scope[:name]}
+ AND kcu.table_schema = #{scope[:schema]}
+ ORDER BY kcu.ordinal_position
SQL
end
@@ -484,7 +406,7 @@ module ActiveRecord
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[:limit], options[:precision], options[:scale], options[:array])
+ sql_type = type_to_sql(type, options)
sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
@@ -492,12 +414,12 @@ module ActiveRecord
if options[:using]
sql << " USING #{options[:using]}"
elsif options[:cast_as]
- cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array])
+ 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_include_default?(options)
+ 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)
end
@@ -587,6 +509,7 @@ module ActiveRecord
end
def foreign_keys(table_name)
+ scope = quoted_scope(table_name)
fk_info = select_all(<<-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
FROM pg_constraint c
@@ -596,8 +519,8 @@ module ActiveRecord
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
JOIN pg_namespace t3 ON c.connamespace = t3.oid
WHERE c.contype = 'f'
- AND t1.relname = #{quote(table_name)}
- AND t3.nspname = ANY (current_schemas(false))
+ AND t1.relname = #{scope[:name]}
+ AND t3.nspname = #{scope[:schema]}
ORDER BY c.conname
SQL
@@ -623,12 +546,8 @@ module ActiveRecord
end
end
- def index_name_length
- 63
- end
-
# Maps logical Rails types to PostgreSQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil)
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
sql = \
case type.to_s
when "binary"
@@ -653,7 +572,7 @@ module ActiveRecord
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.")
end
else
- super(type, limit, precision, scale)
+ super
end
sql << "[]" if array && type != :primary_key
@@ -685,6 +604,39 @@ module ActiveRecord
)
PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
end
+
+ private
+ 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
+
+ sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
+ sql << " WHERE n.nspname = #{scope[:schema]}"
+ sql << " AND c.relname = #{scope[:name]}" if scope[:name]
+ sql << " AND c.relkind IN (#{scope[:type]})"
+ sql
+ end
+
+ def quoted_scope(name = nil, type: nil)
+ schema, name = extract_schema_qualified_name(name)
+ type = \
+ case type
+ when "BASE TABLE"
+ "'r'"
+ when "VIEW"
+ "'v','m'"
+ end
+ scope = {}
+ scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))"
+ scope[:name] = quote(name) if name
+ scope[:type] = type if type
+ scope
+ end
+
+ def extract_schema_qualified_name(string)
+ name = Utils.extract_schema_qualified_name(string.to_s)
+ [name.schema, name.identifier]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index 311988625f..f57179ae59 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -1,6 +1,8 @@
module ActiveRecord
module ConnectionAdapters
class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
+ undef to_yaml if method_defined?(:to_yaml)
+
attr_reader :oid, :fmod, :array
def initialize(type_metadata, oid: nil, fmod: nil)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
index 1412928ca5..aa7940188a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -19,9 +19,9 @@ module ActiveRecord
def quoted
if schema
- PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier)
+ PG::Connection.quote_ident(schema) << SEPARATOR << PG::Connection.quote_ident(identifier)
else
- PGconn.quote_ident(identifier)
+ PG::Connection.quote_ident(identifier)
end
end
@@ -35,6 +35,12 @@ module ActiveRecord
end
protected
+
+ def parts
+ @parts ||= [@schema, @identifier].compact
+ end
+
+ private
def unquote(part)
if part && part.start_with?('"')
part[1..-2]
@@ -42,10 +48,6 @@ module ActiveRecord
part
end
end
-
- def parts
- @parts ||= [@schema, @identifier].compact
- end
end
module Utils # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 710b5cd887..6273614f0c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -3,18 +3,19 @@ gem "pg", "~> 0.18"
require "pg"
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"
-require "active_record/connection_adapters/statement_pool"
module ActiveRecord
module ConnectionHandling # :nodoc:
@@ -28,11 +29,11 @@ module ActiveRecord
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
- # Forward only valid config params to PGconn.connect.
- valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl]
+ # Forward only valid config params to PG::Connection.connect.
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
conn_params.slice!(*valid_conn_param_keys)
- # The postgres drivers don't allow the creation of an unconnected PGconn object,
+ # The postgres drivers don't allow the creation of an unconnected PG::Connection object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
end
@@ -70,7 +71,7 @@ module ActiveRecord
ADAPTER_NAME = "PostgreSQL".freeze
NATIVE_DATABASE_TYPES = {
- primary_key: "serial primary key",
+ primary_key: "bigserial primary key",
string: { name: "character varying" },
text: { name: "text" },
integer: { name: "integer" },
@@ -108,6 +109,8 @@ module ActiveRecord
bit: { name: "bit" },
bit_varying: { name: "bit varying" },
money: { name: "money" },
+ interval: { name: "interval" },
+ oid: { name: "oid" },
}
OID = PostgreSQL::OID #:nodoc:
@@ -198,8 +201,8 @@ module ActiveRecord
end
def connection_active?
- @connection.status == PGconn::CONNECTION_OK
- rescue PGError
+ @connection.status == PG::CONNECTION_OK
+ rescue PG::Error
false
end
end
@@ -212,7 +215,7 @@ module ActiveRecord
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
- @table_alias_length = nil
+ @max_identifier_length = nil
connect
add_pg_encoders
@@ -233,7 +236,9 @@ module ActiveRecord
# Clears the prepared statements cache.
def clear_cache!
- @statements.clear
+ @lock.synchronize do
+ @statements.clear
+ end
end
def truncate(table_name, name = nil)
@@ -244,7 +249,7 @@ module ActiveRecord
def active?
@connection.query "SELECT 1"
true
- rescue PGError
+ rescue PG::Error
false
end
@@ -276,16 +281,6 @@ module ActiveRecord
NATIVE_DATABASE_TYPES
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
- true
- end
-
- # Does PostgreSQL support finding primary key on non-Active Record tables?
- def supports_primary_key? #:nodoc:
- true
- end
-
def set_standard_conforming_strings
execute("SET standard_conforming_strings = on", "SCHEMA")
end
@@ -306,8 +301,8 @@ module ActiveRecord
true
end
- # Range datatypes weren't introduced until PostgreSQL 9.2
def supports_ranges?
+ # Range datatypes weren't introduced until PostgreSQL 9.2
postgresql_version >= 90200
end
@@ -315,6 +310,16 @@ module ActiveRecord
postgresql_version >= 90300
end
+ def supports_pgcrypto_uuid?
+ postgresql_version >= 90400
+ end
+
+ def supports_alter_constraint?
+ # PostgreSQL 9.4 introduces ALTER TABLE ... ALTER CONSTRAINT but it has a bug and fixed in 9.4.2
+ # https://www.postgresql.org/docs/9.4/static/release-9-4-2.html
+ postgresql_version >= 90402
+ 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")
@@ -359,8 +364,9 @@ module ActiveRecord
# Returns the configured supported identifier length supported by PostgreSQL
def table_alias_length
- @table_alias_length ||= query("SHOW max_identifier_length", "SCHEMA")[0][0].to_i
+ @max_identifier_length ||= select_value("SHOW max_identifier_length", "SCHEMA").to_i
end
+ alias index_name_length table_alias_length
# Set the authorized user for this session
def session_auth=(user)
@@ -372,10 +378,6 @@ module ActiveRecord
@use_insert_returning
end
- def valid_type?(type)
- !native_database_types[type].nil?
- end
-
def update_table_definition(table_name, base) #:nodoc:
PostgreSQL::Table.new(table_name, base)
end
@@ -400,10 +402,16 @@ module ActiveRecord
@connection.server_version
end
- protected
+ def default_index_type?(index) # :nodoc:
+ index.using == :btree || super
+ end
+
+ private
# See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
+ NUMERIC_VALUE_OUT_OF_RANGE = "22003"
+ NOT_NULL_VIOLATION = "23502"
FOREIGN_KEY_VIOLATION = "23503"
UNIQUE_VIOLATION = "23505"
SERIALIZATION_FAILURE = "40001"
@@ -412,13 +420,17 @@ module ActiveRecord
def translate_exception(exception, message)
return exception unless exception.respond_to?(:result)
- case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE)
+ case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
RecordNotUnique.new(message)
when FOREIGN_KEY_VIOLATION
InvalidForeignKey.new(message)
when VALUE_LIMIT_VIOLATION
ValueTooLong.new(message)
+ when NUMERIC_VALUE_OUT_OF_RANGE
+ RangeError.new(message)
+ when NOT_NULL_VIOLATION
+ NotNullViolation.new(message)
when SERIALIZATION_FAILURE
SerializationFailure.new(message)
when DEADLOCK_DETECTED
@@ -428,9 +440,7 @@ module ActiveRecord
end
end
- private
-
- def get_oid_type(oid, fmod, column_name, sql_type = "") # :nodoc:
+ def get_oid_type(oid, fmod, column_name, sql_type = "".freeze)
if !type_map.key?(oid)
load_additional_types(type_map, [oid])
end
@@ -443,11 +453,11 @@ module ActiveRecord
}
end
- def initialize_type_map(m) # :nodoc:
+ def initialize_type_map(m)
register_class_with_limit m, "int2", Type::Integer
register_class_with_limit m, "int4", Type::Integer
register_class_with_limit m, "int8", Type::Integer
- m.alias_type "oid", "int2"
+ m.register_type "oid", OID::Oid.new
m.register_type "float4", Type::Float.new
m.alias_type "float8", "float4"
m.register_type "text", Type::Text.new
@@ -482,8 +492,10 @@ module ActiveRecord
m.register_type "polygon", OID::SpecializedString.new(:polygon)
m.register_type "circle", OID::SpecializedString.new(:circle)
- # FIXME: why are we keeping these types as strings?
- m.alias_type "interval", "varchar"
+ m.register_type "interval" do |_, _, sql_type|
+ precision = extract_precision(sql_type)
+ OID::SpecializedString.new(:interval, precision: precision)
+ end
register_class_with_precision m, "time", Type::Time
register_class_with_precision m, "timestamp", OID::DateTime
@@ -511,7 +523,7 @@ module ActiveRecord
load_additional_types(m)
end
- def extract_limit(sql_type) # :nodoc:
+ def extract_limit(sql_type)
case sql_type
when /^bigint/i, /^int8/i
8
@@ -523,7 +535,7 @@ module ActiveRecord
end
# Extracts the value from a PostgreSQL column default definition.
- def extract_value_from_default(default) # :nodoc:
+ def extract_value_from_default(default)
case default
# Quoted types
when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
@@ -549,15 +561,15 @@ module ActiveRecord
end
end
- def extract_default_function(default_value, default) # :nodoc:
+ def extract_default_function(default_value, default)
default if has_default_function?(default_value, default)
end
- def has_default_function?(default_value, default) # :nodoc:
+ def has_default_function?(default_value, default)
!default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default)
end
- def load_additional_types(type_map, oids = nil) # :nodoc:
+ def load_additional_types(type_map, oids = nil)
initializer = OID::TypeMapInitializer.new(type_map)
if supports_ranges?
@@ -625,8 +637,10 @@ module ActiveRecord
if in_transaction?
raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
else
- # outside of transactions we can simply flush this query and retry
- @statements.delete sql_key(sql)
+ @lock.synchronize do
+ # outside of transactions we can simply flush this query and retry
+ @statements.delete sql_key(sql)
+ end
retry
end
end
@@ -643,7 +657,7 @@ module ActiveRecord
CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze
def is_cached_plan_failure?(e)
pgerror = e.cause
- code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
rescue
false
@@ -662,25 +676,27 @@ module ActiveRecord
# Prepare the statement if it hasn't been prepared, return
# the statement key.
def prepare_statement(sql)
- sql_key = sql_key(sql)
- unless @statements.key? sql_key
- nextkey = @statements.next_key
- begin
- @connection.prepare nextkey, sql
- rescue => e
- raise translate_exception_class(e, sql)
+ @lock.synchronize do
+ sql_key = sql_key(sql)
+ unless @statements.key? sql_key
+ nextkey = @statements.next_key
+ begin
+ @connection.prepare nextkey, sql
+ rescue => e
+ raise translate_exception_class(e, sql)
+ end
+ # Clear the queue
+ @connection.get_last_result
+ @statements[sql_key] = nextkey
end
- # Clear the queue
- @connection.get_last_result
- @statements[sql_key] = nextkey
+ @statements[sql_key]
end
- @statements[sql_key]
end
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
- @connection = PGconn.connect(@connection_parameters)
+ @connection = PG.connect(@connection_parameters)
configure_connection
rescue ::PG::Error => error
if error.message.include?("does not exist")
@@ -725,7 +741,7 @@ module ActiveRecord
end
# Returns the current ID of a table's sequence.
- def last_insert_id_result(sequence_name) # :nodoc:
+ def last_insert_id_result(sequence_name)
exec_query("SELECT currval('#{sequence_name}')", "SQL")
end
@@ -747,27 +763,27 @@ module ActiveRecord
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
- def column_definitions(table_name) # :nodoc:
+ def column_definitions(table_name)
query(<<-end_sql, "SCHEMA")
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
- (SELECT c.collname FROM pg_collation c, pg_type t
- WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
- col_description(a.attrelid, a.attnum) AS comment
- FROM pg_attribute a LEFT JOIN pg_attrdef d
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ c.collname, col_description(a.attrelid, a.attnum) AS comment
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ LEFT JOIN pg_type t ON a.atttypid = t.oid
+ LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
end_sql
end
- def extract_table_ref_from_insert_sql(sql) # :nodoc:
+ def extract_table_ref_from_insert_sql(sql)
sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
$1.strip if $1
end
- def create_table_definition(*args) # :nodoc:
+ def create_table_definition(*args)
PostgreSQL::TableDefinition.new(*args)
end
@@ -798,7 +814,6 @@ module ActiveRecord
map[Integer] = PG::TextEncoder::Integer.new
map[TrueClass] = PG::TextEncoder::Boolean.new
map[FalseClass] = PG::TextEncoder::Boolean.new
- map[Float] = PG::TextEncoder::Float.new
@connection.type_map_for_queries = map
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 8219f132c3..4d339b0a8c 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -21,6 +21,22 @@ module ActiveRecord
@data_sources = @data_sources.dup
end
+ def encode_with(coder)
+ coder["columns"] = @columns
+ coder["columns_hash"] = @columns_hash
+ coder["primary_keys"] = @primary_keys
+ coder["data_sources"] = @data_sources
+ coder["version"] = ActiveRecord::Migrator.current_version
+ end
+
+ def init_with(coder)
+ @columns = coder["columns"]
+ @columns_hash = coder["columns_hash"]
+ @primary_keys = coder["primary_keys"]
+ @data_sources = coder["data_sources"]
+ @version = coder["version"]
+ end
+
def primary_keys(table_name)
@primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
end
@@ -32,8 +48,6 @@ module ActiveRecord
@data_sources[name] = connection.data_source_exists?(name)
end
- alias table_exists? data_source_exists?
- deprecate table_exists?: "use #data_source_exists? instead"
# Add internal cache for table with +table_name+.
def add(table_name)
@@ -47,8 +61,6 @@ module ActiveRecord
def data_sources(name)
@data_sources[name]
end
- alias tables data_sources
- deprecate tables: "use #data_sources instead"
# Get the columns for a table
def columns(table_name)
@@ -83,8 +95,6 @@ module ActiveRecord
@primary_keys.delete name
@data_sources.delete name
end
- alias clear_table_cache! clear_data_source_cache!
- deprecate clear_table_cache!: "use #clear_data_source_cache! instead"
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index f01ed67b0f..7276a65098 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -18,15 +18,11 @@ module ActiveRecord
quoted_date(value)
end
- private
+ def quoted_binary(value)
+ "x'#{value.hex}'"
+ end
- def _quote(value)
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
- super
- end
- end
+ private
def _type_cast(value)
case value
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
index 70c0d28830..bc798d1dbb 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
@@ -1,15 +1,8 @@
module ActiveRecord
module ConnectionAdapters
module SQLite3
- class SchemaCreation < AbstractAdapter::SchemaCreation
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
private
-
- def column_options(o)
- options = super
- options[:null] = false if o.primary_key
- options
- end
-
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
new file mode 100644
index 0000000000..e157e4b218
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
@@ -0,0 +1,28 @@
+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
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb
new file mode 100644
index 0000000000..eec018eda3
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module ColumnDumper # :nodoc:
+ private
+
+ def default_primary_key?(column)
+ schema_type(column) == :integer
+ end
+
+ def explicit_primary_key_default?(column)
+ column.bigint?
+ end
+ 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
new file mode 100644
index 0000000000..4ba245a0d8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module SchemaStatements # :nodoc:
+ private
+ def data_source_sql(name = nil, type: nil)
+ scope = quoted_scope(name, type: type)
+ scope[:type] ||= "'table','view'"
+
+ sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'"
+ sql << " AND name = #{scope[:name]}" if scope[:name]
+ sql << " AND type IN (#{scope[:type]})"
+ sql
+ end
+
+ def quoted_scope(name = nil, type: nil)
+ type = \
+ case type
+ when "BASE TABLE"
+ "'table'"
+ when "VIEW"
+ "'view'"
+ end
+ scope = {}
+ scope[:name] = quote(name) if name
+ scope[:type] = type if type
+ scope
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 0493ab4e4b..d24bfc0c93 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -3,6 +3,9 @@ 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"
@@ -52,6 +55,8 @@ module ActiveRecord
ADAPTER_NAME = "SQLite".freeze
include SQLite3::Quoting
+ include SQLite3::ColumnDumper
+ include SQLite3::SchemaStatements
NATIVE_DATABASE_TYPES = {
primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL",
@@ -75,6 +80,10 @@ module ActiveRecord
end
end
+ def update_table_definition(table_name, base) # :nodoc:
+ SQLite3::Table.new(table_name, base)
+ end
+
def schema_creation # :nodoc:
SQLite3::SchemaCreation.new self
end
@@ -88,6 +97,8 @@ module ActiveRecord
@active = nil
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
+
+ configure_connection
end
def supports_ddl_transactions?
@@ -108,17 +119,12 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations? #:nodoc:
+ def requires_reloading?
true
end
- def supports_primary_key? #:nodoc:
- true
- end
-
- def requires_reloading?
- true
+ def supports_foreign_keys_in_create?
+ sqlite_version >= "3.6.19"
end
def supports_views?
@@ -154,10 +160,6 @@ module ActiveRecord
true
end
- def valid_type?(type)
- true
- end
-
# Returns 62. SQLite supports index names up to 64
# characters. The rest is used by Rails internally to perform
# temporary rename operations
@@ -178,6 +180,19 @@ module ActiveRecord
true
end
+ # REFERENTIAL INTEGRITY ====================================
+
+ def disable_referential_integrity # :nodoc:
+ old = select_value("PRAGMA foreign_keys")
+
+ begin
+ execute("PRAGMA foreign_keys = OFF")
+ yield
+ ensure
+ execute("PRAGMA foreign_keys = #{old}")
+ end
+ end
+
#--
# DATABASE STATEMENTS ======================================
#++
@@ -252,58 +267,6 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def tables(name = nil) # :nodoc:
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #tables currently returns both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
- Use #data_sources instead.
- MSG
-
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing arguments to #tables is deprecated without replacement.
- MSG
- end
-
- data_sources
- end
-
- def data_sources
- select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", "SCHEMA")
- end
-
- def table_exists?(table_name)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #table_exists? currently checks both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
- Use #data_source_exists? instead.
- MSG
-
- data_source_exists?(table_name)
- end
-
- def data_source_exists?(table_name)
- return false unless table_name.present?
-
- sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'"
- sql << " AND name = #{quote(table_name)}"
-
- select_values(sql, "SCHEMA").any?
- end
-
- def views # :nodoc:
- select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA")
- end
-
- def view_exists?(view_name) # :nodoc:
- return false unless view_name.present?
-
- sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'"
- sql << " AND name = #{quote(view_name)}"
-
- select_values(sql, "SCHEMA").any?
- end
-
def new_column_from_field(table_name, field) # :nondoc:
case field["dflt_value"]
when /^null$/i
@@ -322,6 +285,12 @@ module ActiveRecord
# 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
+
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row|
sql = <<-SQL
SELECT sql
@@ -405,11 +374,10 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
- include_default = options_include_default?(options)
definition[column_name].instance_eval do
self.type = type
self.limit = options[:limit] if options.include?(:limit)
- self.default = options[:default] if include_default
+ self.default = options[:default] if options.include?(:default)
self.null = options[:null] if options.include?(:null)
self.precision = options[:precision] if options.include?(:precision)
self.scale = options[:scale] if options.include?(:scale)
@@ -424,16 +392,34 @@ module ActiveRecord
rename_column_indexes(table_name, column.name, new_column_name)
end
- protected
+ def add_reference(table_name, ref_name, **options) # :nodoc:
+ super(table_name, ref_name, type: :integer, **options)
+ end
+ alias :add_belongs_to :add_reference
+
+ def foreign_keys(table_name)
+ fk_info = select_all("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
+ fk_info.map do |row|
+ options = {
+ column: row["from"],
+ primary_key: row["to"],
+ on_delete: extract_foreign_key_action(row["on_delete"]),
+ on_update: extract_foreign_key_action(row["on_update"])
+ }
+ ForeignKeyDefinition.new(table_name, row["table"], options)
+ end
+ end
- def table_structure(table_name) # :nodoc:
+ private
+
+ def table_structure(table_name)
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
table_structure_with_collation(table_name, structure)
end
alias column_definitions table_structure
- def alter_table(table_name, options = {}) #:nodoc:
+ def alter_table(table_name, options = {})
altered_table_name = "a#{table_name}"
caller = lambda { |definition| yield definition if block_given? }
@@ -444,12 +430,12 @@ module ActiveRecord
end
end
- def move_table(from, to, options = {}, &block) #:nodoc:
+ def move_table(from, to, options = {}, &block)
copy_table(from, to, options, &block)
drop_table(from)
end
- def copy_table(from, to, options = {}) #:nodoc:
+ def copy_table(from, to, options = {})
from_primary_key = primary_key(from)
options[:id] = false
create_table(to, options) do |definition|
@@ -475,7 +461,7 @@ module ActiveRecord
options[:rename] || {})
end
- def copy_table_indexes(from, to, rename = {}) #:nodoc:
+ def copy_table_indexes(from, to, rename = {})
indexes(from).each do |index|
name = index.name
if to == "a#{from}"
@@ -498,7 +484,7 @@ module ActiveRecord
end
end
- def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
+ def copy_table_contents(from, to, columns, rename = {})
column_mappings = Hash[columns.map { |name| [name, name] }]
rename.each { |a| column_mappings[a.last] = a.first }
from_columns = columns(from).collect(&:name)
@@ -512,7 +498,7 @@ module ActiveRecord
end
def sqlite_version
- @sqlite_version ||= SQLite3Adapter::Version.new(select_value("select sqlite_version(*)"))
+ @sqlite_version ||= SQLite3Adapter::Version.new(select_value("SELECT sqlite_version(*)"))
end
def translate_exception(exception, message)
@@ -523,12 +509,15 @@ module ActiveRecord
# column *column_name* is not unique
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
RecordNotUnique.new(message)
+ when /.* may not be NULL/, /NOT NULL constraint failed: .*/
+ NotNullViolation.new(message)
+ when /FOREIGN KEY constraint failed/i
+ InvalidForeignKey.new(message)
else
super
end
end
- private
COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
def table_structure_with_collation(table_name, basic_structure)
@@ -569,6 +558,22 @@ module ActiveRecord
basic_structure.to_hash
end
end
+
+ def create_table_definition(*args)
+ SQLite3::TableDefinition.new(*args)
+ end
+
+ def extract_foreign_key_action(specifier)
+ case specifier
+ when "CASCADE"; :cascade
+ when "SET NULL"; :nullify
+ when "RESTRICT"; :restrict
+ end
+ end
+
+ def configure_connection
+ execute("PRAGMA foreign_keys = ON", "SCHEMA")
+ end
end
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 1fbe374ade..8f78330d4a 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -171,41 +171,37 @@ module ActiveRecord
return super if block_given? ||
primary_key.nil? ||
scope_attributes? ||
- columns_hash.include?(inheritance_column) ||
- ids.first.kind_of?(Array)
+ columns_hash.include?(inheritance_column)
id = ids.first
- if ActiveRecord::Base === id
- id = id.id
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- You are passing an instance of ActiveRecord::Base to `find`.
- Please pass the id of the object by calling `.id`.
- MSG
- end
+
+ return super if id.kind_of?(Array) ||
+ id.is_a?(ActiveRecord::Base)
key = primary_key
statement = cached_find_by_statement(key) { |params|
where(key => params.bind).limit(1)
}
+
record = statement.execute([id], self, connection).first
unless record
raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
name, primary_key, id)
end
record
- rescue RangeError
+ rescue ::RangeError
raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'",
name, primary_key)
end
def find_by(*args) # :nodoc:
- return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any?
+ return super if scope_attributes? || reflect_on_all_aggregations.any?
hash = args.first
- return super if hash.values.any? { |v|
- v.nil? || Array === v || Hash === v || Relation === v
+ return super if !(Hash === hash) || hash.values.any? { |v|
+ v.nil? || Array === v || Hash === v || Relation === v || Base === v
}
# We can't cache Post.find_by(author: david) ...yet
@@ -223,7 +219,7 @@ module ActiveRecord
statement.execute(hash.values, self, connection).first
rescue TypeError
raise ActiveRecord::StatementInvalid
- rescue RangeError
+ rescue ::RangeError
nil
end
end
@@ -239,7 +235,9 @@ module ActiveRecord
def generated_association_methods
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
+ private_constant :GeneratedAssociationMethods
include mod
+
mod
end
end
@@ -299,24 +297,24 @@ module ActiveRecord
private
- def cached_find_by_statement(key, &block) # :nodoc:
+ def cached_find_by_statement(key, &block)
cache = @find_by_statement_cache[connection.prepared_statements]
cache[key] || cache.synchronize {
cache[key] ||= StatementCache.create(connection, &block)
}
end
- def relation # :nodoc:
+ def relation
relation = Relation.create(self, arel_table, predicate_builder)
if finder_needs_type_condition? && !ignore_default_scope?
- relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
+ relation.where(type_condition).create_with(inheritance_column.to_s => sti_name)
else
relation
end
end
- def table_metadata # :nodoc:
+ def table_metadata
TableMetadata.new(self, arel_table)
end
end
@@ -452,7 +450,7 @@ module ActiveRecord
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
if id
- self.class.hash ^ self.id.hash
+ self.class.hash ^ id.hash
else
super
end
@@ -474,7 +472,7 @@ module ActiveRecord
# Allows sort on objects
def <=>(other_object)
if other_object.is_a?(self.class)
- self.to_key <=> other_object.to_key
+ to_key <=> other_object.to_key
else
super
end
@@ -561,7 +559,6 @@ module ActiveRecord
@marked_for_destruction = false
@destroyed_by_association = nil
@new_record = true
- @txn = nil
@_start_transaction_state = {}
@transaction_state = nil
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index e2da512813..cbd71a3779 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -12,13 +12,21 @@ module ActiveRecord
#
# * +id+ - The id of the object you wish to reset a counter on.
# * +counters+ - One or more association counters to reset. Association name or counter name can be given.
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
+ # touch that column or an array of symbols to touch just those ones.
#
# ==== Examples
#
- # # For Post with id #1 records reset the comments_count
+ # # For the Post with id #1, reset the comments_count
# Post.reset_counters(1, :comments)
- def reset_counters(id, *counters)
+ #
+ # # Like above, but also touch the +updated_at+ and/or +updated_on+
+ # # attributes.
+ # Post.reset_counters(1, :comments, touch: true)
+ def reset_counters(id, *counters, touch: nil)
object = find(id)
+
counters.each do |counter_association|
has_many_association = _reflect_on_association(counter_association)
unless has_many_association
@@ -37,10 +45,12 @@ module ActiveRecord
reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
counter_name = reflection.counter_cache_column
- unscoped.where(primary_key => object.id).update_all(
- counter_name => object.send(counter_association).count(:all)
- )
+ updates = { counter_name.to_sym => object.send(counter_association).count(:all) }
+ updates.merge!(touch_updates(touch)) if touch
+
+ unscoped.where(primary_key => object.id).update_all(updates)
end
+
return true
end
@@ -55,6 +65,9 @@ module ActiveRecord
# * +id+ - The id of the object you wish to update a counter on or an array of ids.
# * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
+ # * <tt>:touch</tt> option - Touch timestamp columns when updating.
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
+ # touch that column or an array of symbols to touch just those ones.
#
# ==== Examples
#
@@ -73,13 +86,29 @@ module ActiveRecord
# # UPDATE posts
# # SET comment_count = COALESCE(comment_count, 0) + 1
# # WHERE id IN (10, 15)
+ #
+ # # For the Posts with id of 10 and 15, increment the comment_count by 1
+ # # and update the updated_at value for each counter.
+ # Post.update_counters [10, 15], comment_count: 1, touch: true
+ # # Executes the following SQL:
+ # # UPDATE posts
+ # # SET comment_count = COALESCE(comment_count, 0) + 1,
+ # # `updated_at` = '2016-10-13T09:59:23-05:00'
+ # # WHERE id IN (10, 15)
def update_counters(id, counters)
+ touch = counters.delete(:touch)
+
updates = counters.map do |counter_name, value|
operator = value < 0 ? "-" : "+"
quoted_column = connection.quote_column_name(counter_name)
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
+ if touch
+ touch_updates = touch_updates(touch)
+ updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty?
+ end
+
unscoped.where(primary_key => id).update_all updates.join(", ")
end
@@ -94,13 +123,20 @@ module ActiveRecord
#
# * +counter_name+ - The name of the field that should be incremented.
# * +id+ - The id of the object that should be incremented or an array of ids.
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
+ # touch that column or an array of symbols to touch just those ones.
#
# ==== Examples
#
# # Increment the posts_count column for the record with an id of 5
# DiscussionBoard.increment_counter(:posts_count, 5)
- def increment_counter(counter_name, id)
- update_counters(id, counter_name => 1)
+ #
+ # # Increment the posts_count column for the record with an id of 5
+ # # and update the updated_at value.
+ # DiscussionBoard.increment_counter(:posts_count, 5, touch: true)
+ def increment_counter(counter_name, id, touch: nil)
+ update_counters(id, counter_name => 1, touch: touch)
end
# Decrement a numeric field by one, via a direct SQL update.
@@ -112,14 +148,28 @@ module ActiveRecord
#
# * +counter_name+ - The name of the field that should be decremented.
# * +id+ - The id of the object that should be decremented or an array of ids.
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
+ # touch that column or an array of symbols to touch just those ones.
#
# ==== Examples
#
# # Decrement the posts_count column for the record with an id of 5
# DiscussionBoard.decrement_counter(:posts_count, 5)
- def decrement_counter(counter_name, id)
- update_counters(id, counter_name => -1)
+ #
+ # # Decrement the posts_count column for the record with an id of 5
+ # # and update the updated_at value.
+ # DiscussionBoard.decrement_counter(:posts_count, 5, touch: true)
+ def decrement_counter(counter_name, id, touch: nil)
+ update_counters(id, counter_name => -1, touch: touch)
end
+
+ private
+ def touch_updates(touch)
+ touch = timestamp_attributes_for_update_in_model if touch == true
+ touch_time = current_time_from_proper_timezone
+ Array(touch).map { |column| [ column, touch_time ] }.to_h
+ end
end
private
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 0a94ab58dd..0ab03b2ab3 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -140,6 +140,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, :mapping, :subtype
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 6464d40c94..18fac5af1b 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -95,19 +95,9 @@ module ActiveRecord
#
# Wraps the underlying database error as +cause+.
class StatementInvalid < ActiveRecordError
- def initialize(message = nil, original_exception = nil)
- if original_exception
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
+ def initialize(message = nil)
super(message || $!.try(:message))
end
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
end
# Defunct wrapper class kept for compatibility.
@@ -123,10 +113,46 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
+ # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type.
+ class MismatchedForeignKey < StatementInvalid
+ 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
+ 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
+ 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
+ end
+ if message
+ msg << "\nOriginal message: #{message}"
+ end
+ super(msg)
+ end
+
+ private
+ def column_type(table, column)
+ @adapter.columns(table).detect { |c| c.name == column }.sql_type
+ end
+ end
+
+ # Raised when a record cannot be inserted or updated because it would violate a not null constraint.
+ class NotNullViolation < StatementInvalid
+ end
+
# Raised when a record cannot be inserted or updated because a value too long for a column type.
class ValueTooLong < StatementInvalid
end
+ # Raised when values that executed are out of range.
+ class RangeError < StatementInvalid
+ end
+
# Raised when number of bind variables in statement given to +:condition+ key
# (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method)
# does not match number of expected values supplied.
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 980b8e1baa..8f7ae2c33c 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,4 +1,3 @@
-require "active_support/lazy_load_hooks"
require "active_record/explain_registry"
module ActiveRecord
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 5ba354d758..6cf2e01179 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -66,10 +66,13 @@ module ActiveRecord
# Validate our unmarshalled data.
def validate(data)
unless Hash === data || YAML::Omap === data
- raise Fixture::FormatError, "fixture is not a hash"
+ raise Fixture::FormatError, "fixture is not a hash: #{@file}"
end
- raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
+ invalid = data.reject { |_, row| Hash === row }
+ if invalid.any?
+ raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}"
+ end
data
end
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 21c5e5b5bb..e79167d568 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -536,16 +536,16 @@ module ActiveRecord
update_all_loaded_fixtures fixtures_map
connection.transaction(requires_new: true) do
- deleted_tables = Set.new
+ 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
table_rows.each_key do |table|
- unless deleted_tables.include? table
+ unless deleted_tables[conn].include? table
conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete"
end
- deleted_tables << table
+ deleted_tables[conn] << table
end
table_rows.each do |fixture_set_name, rows|
@@ -862,29 +862,17 @@ module ActiveRecord
class_attribute :fixture_table_names
class_attribute :fixture_class_names
class_attribute :use_transactional_tests
- class_attribute :use_transactional_fixtures
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
class_attribute :config
- singleton_class.deprecate "use_transactional_fixtures=" => "use use_transactional_tests= instead"
-
self.fixture_table_names = []
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
self.config = ActiveRecord::Base
self.fixture_class_names = {}
-
- silence_warnings do
- define_singleton_method :use_transactional_tests do
- if use_transactional_fixtures.nil?
- true
- else
- use_transactional_fixtures
- end
- end
- end
+ self.use_transactional_tests = true
end
module ClassMethods
@@ -897,7 +885,7 @@ module ActiveRecord
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
def set_fixture_class(class_names = {})
- self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
+ self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
@@ -982,6 +970,7 @@ module ActiveRecord
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.begin_transaction joinable: false
+ connection.pool.lock_thread = true
end
# When connections are established in the future, begin a transaction too
@@ -997,6 +986,7 @@ module ActiveRecord
if connection && !@fixture_connections.include?(connection)
connection.begin_transaction joinable: false
+ connection.pool.lock_thread = true
@fixture_connections << connection
end
end
@@ -1019,6 +1009,7 @@ module ActiveRecord
ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
@fixture_connections.each do |connection|
connection.rollback_transaction if connection.transaction_open?
+ connection.pool.lock_thread = false
end
@fixture_connections.clear
else
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index f33456a744..1a937dbcf7 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -6,7 +6,7 @@ module ActiveRecord
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index a1d4f47372..fbdaeaae51 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -130,16 +130,26 @@ module ActiveRecord
store_full_sti_class ? name : name.demodulize
end
+ def inherited(subclass)
+ subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
+ super
+ end
+
protected
# Returns the class type of the record using the current module as a prefix. So descendants of
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
def compute_type(type_name)
- if type_name.match(/^::/)
+ if type_name.start_with?("::".freeze)
# If the type is prefixed with a scope operator then we assume that
# the type_name is an absolute reference.
ActiveSupport::Dependencies.constantize(type_name)
else
+ type_candidate = @_type_candidates_cache[type_name]
+ if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate)
+ return type_constant
+ end
+
# Build a list of candidates to search for
candidates = []
name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
@@ -147,7 +157,10 @@ module ActiveRecord
candidates.each do |candidate|
constant = ActiveSupport::Dependencies.safe_constantize(candidate)
- return constant if candidate == constant.to_s
+ if candidate == constant.to_s
+ @_type_candidates_cache[type_name] = candidate
+ return constant
+ end
end
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 20d61dba67..25ee9d6bfe 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -23,7 +23,7 @@ module ActiveRecord
end
def table_exists?
- ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) }
+ connection.table_exists?(table_name)
end
# Creates an internal metadata table with columns +key+ and +value+
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 82882469e3..78ce9f8291 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -47,8 +47,6 @@ module ActiveRecord
# self.locking_column = :lock_person
# end
#
- # Please note that the optimistic locking will be ignored if you update the
- # locking column's value.
module Optimistic
extend ActiveSupport::Concern
@@ -69,7 +67,7 @@ module ActiveRecord
send(lock_col + "=", previous_lock_value + 1)
end
- def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+ 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
# a change from the default, since the database might have no default
@@ -78,15 +76,13 @@ module ActiveRecord
super
end
- def _update_record(attribute_names = self.attribute_names) #:nodoc:
+ def _update_record(attribute_names = self.attribute_names)
return super unless locking_enabled?
-
- lock_col = self.class.locking_column
-
- return super if attribute_names.include?(lock_col)
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
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index e73cb4fc12..263e2a5f7f 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -59,7 +59,16 @@ module ActiveRecord
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
# the locked record.
def lock!(lock = true)
- reload(lock: lock) if persisted?
+ 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.
+ MSG
+ end
+ reload(lock: lock)
+ end
self
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 4b8d8d9105..2297c77835 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -44,17 +44,17 @@ module ActiveRecord
private
def type_casted_binds(binds, casted_binds)
- casted_binds || binds.map { |attr| type_cast attr.value_for_database }
+ casted_binds || ActiveRecord::Base.connection.type_casted_binds(binds)
end
- def render_bind(attr, type_casted_value)
- value = if attr.type.binary? && attr.value
- "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
- else
- type_casted_value
+ def render_bind(attr, value)
+ if attr.is_a?(Array)
+ attr = attr.first
+ elsif attr.type.binary? && attr.value
+ value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
end
- [attr.name, value]
+ [attr && attr.name, value]
end
def colorize_payload_name(name, payload_name)
@@ -81,7 +81,7 @@ module ActiveRecord
RED
when /transaction\s*\Z/i
CYAN
- else
+ else
MAGENTA
end
end
@@ -89,10 +89,6 @@ module ActiveRecord
def logger
ActiveRecord::Base.logger
end
-
- def type_cast(value)
- ActiveRecord::Base.connection.type_cast(value)
- end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index cc6bc17b9d..4e1df1432c 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -522,7 +522,10 @@ module ActiveRecord
def self.inherited(subclass) # :nodoc:
super
if subclass.superclass == Migration
- subclass.include Compatibility::Legacy
+ raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \
+ "Please specify the Rails release the migration was written for:\n" \
+ "\n" \
+ " class #{subclass} < ActiveRecord::Migration[4.2]"
end
end
@@ -545,12 +548,10 @@ module ActiveRecord
end
def call(env)
- if connection.supports_migrations?
- mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
- if @last_check < mtime
- ActiveRecord::Migration.check_pending!(connection)
- @last_check = mtime
- end
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
end
@app.call(env)
end
@@ -689,7 +690,7 @@ module ActiveRecord
connection.respond_to?(:reverting) && connection.reverting
end
- class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
+ ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc:
def up
yield unless reverting
end
@@ -935,7 +936,7 @@ module ActiveRecord
# MigrationProxy is used to defer loading of the actual migration classes
# until they are needed
- class MigrationProxy < Struct.new(:name, :version, :filename, :scope)
+ MigrationProxy = Struct.new(:name, :version, :filename, :scope) do
def initialize(name, version, filename, scope)
super
@migration = nil
@@ -1024,14 +1025,13 @@ module ActiveRecord
def schema_migrations_table_name
SchemaMigration.table_name
end
+ deprecate :schema_migrations_table_name
def get_all_versions(connection = Base.connection)
- ActiveSupport::Deprecation.silence do
- if connection.table_exists?(schema_migrations_table_name)
- SchemaMigration.all.map { |x| x.version.to_i }.sort
- else
- []
- end
+ if SchemaMigration.table_exists?
+ SchemaMigration.all_versions.map(&:to_i)
+ else
+ []
end
end
@@ -1057,10 +1057,6 @@ module ActiveRecord
Array(@migrations_paths)
end
- def match_to_migration_filename?(filename) # :nodoc:
- Migration::MigrationFilenameRegexp.match?(File.basename(filename))
- end
-
def parse_migration_filename(filename) # :nodoc:
File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
end
@@ -1068,9 +1064,7 @@ module ActiveRecord
def migrations(paths)
paths = Array(paths)
- files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]
-
- migrations = files.map do |file|
+ migrations = migration_files(paths).map do |file|
version, name, scope = parse_migration_filename(file)
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
@@ -1082,6 +1076,30 @@ module ActiveRecord
migrations.sort_by(&:version)
end
+ def migrations_status(paths)
+ paths = Array(paths)
+
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ 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
+
+ db_list.map! do |version|
+ ["up", version, "********** NO FILE **********"]
+ end
+
+ (db_list + file_list).sort_by { |_, version, _| version }
+ end
+
+ def migration_files(paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
+ end
+
private
def move(direction, migrations_paths, steps)
@@ -1097,8 +1115,6 @@ module ActiveRecord
end
def initialize(direction, migrations, target_version = nil)
- raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
-
@direction = direction
@target_version = target_version
@migrated_versions = nil
@@ -1106,8 +1122,8 @@ module ActiveRecord
validate(@migrations)
- Base.connection.initialize_schema_migrations_table
- Base.connection.initialize_internal_metadata_table
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::InternalMetadata.create_table
end
def current_version
@@ -1169,9 +1185,10 @@ module ActiveRecord
def run_without_lock
migration = migrations.detect { |m| m.version == @target_version }
raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
- execute_migration_in_transaction(migration, @direction)
+ result = execute_migration_in_transaction(migration, @direction)
record_environment
+ result
end
# Used for running multiple migrations up to or down to a certain value.
@@ -1180,11 +1197,12 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- runnable.each do |migration|
+ result = runnable.each do |migration|
execute_migration_in_transaction(migration, @direction)
end
record_environment
+ result
end
# Stores the current environment in the database.
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 04e538baa5..188dd0acef 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -11,9 +11,71 @@ module ActiveRecord
const_get(name)
end
- V5_1 = Current
+ V5_2 = Current
- module FourTwoShared
+ class V5_1 < V5_2
+ end
+
+ class V5_0 < V5_1
+ module TableDefinition
+ def references(*args, **options)
+ super(*args, type: :integer, **options)
+ end
+ alias :belongs_to :references
+ end
+
+ def create_table(table_name, options = {})
+ if adapter_name == "PostgreSQL"
+ if options[:id] == :uuid && !options.key?(:default)
+ options[:default] = "uuid_generate_v4()"
+ end
+ end
+
+ unless adapter_name == "Mysql2" && options[:id] == :bigint
+ if [:integer, :bigint].include?(options[:id]) && !options.key?(:default)
+ options[:default] = nil
+ end
+ end
+
+ # Since 5.1 Postgres adapter uses bigserial type for primary
+ # keys by default and MySQL uses bigint. This compat layer makes old migrations utilize
+ # serial/int type instead -- the way it used to work before 5.1.
+ unless options.key?(:id)
+ options[:id] = :integer
+ end
+
+ if block_given?
+ super(table_name, options) do |t|
+ class << t
+ prepend TableDefinition
+ end
+ yield t
+ end
+ else
+ super
+ end
+ end
+
+ def change_table(table_name, options = {})
+ if block_given?
+ super(table_name, options) do |t|
+ class << t
+ prepend TableDefinition
+ end
+ yield t
+ end
+ else
+ super
+ end
+ end
+
+ def add_reference(table_name, ref_name, **options)
+ super(table_name, ref_name, type: :integer, **options)
+ end
+ alias :add_belongs_to :add_reference
+ end
+
+ class V4_2 < V5_0
module TableDefinition
def references(*, **options)
options[:index] ||= false
@@ -86,13 +148,13 @@ module ActiveRecord
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
- unless index_name_exists?(table_name, index_name, true)
+ unless index_name_exists?(table_name, index_name)
if options.is_a?(Hash) && options.has_key?(:name)
options_without_column = options.dup
options_without_column.delete :column
index_name_without_column = index_name(table_name, options_without_column)
- return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
+ return index_name_without_column if index_name_exists?(table_name, index_name_without_column)
end
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
@@ -101,29 +163,6 @@ module ActiveRecord
index_name
end
end
-
- class V5_0 < V5_1
- end
-
- class V4_2 < V5_0
- # 4.2 is defined as a module because it needs to be shared with
- # Legacy. When the time comes, V5_0 should be defined straight
- # in its class.
- include FourTwoShared
- end
-
- module Legacy
- include FourTwoShared
-
- def migrate(*)
- ActiveSupport::Deprecation.warn \
- "Directly inheriting from ActiveRecord::Migration is deprecated. " \
- "Please specify the Rails release the migration was written for:\n" \
- "\n" \
- " class #{self.class.name} < ActiveRecord::Migration[4.2]"
- super
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 2a28c6bf6d..54216caaaf 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -432,6 +432,7 @@ module ActiveRecord
connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
+ initialize_find_by_cache
end
private
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index e983026961..01ecd79b8f 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -393,7 +393,7 @@ module ActiveRecord
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
# then the existing record will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
- options = self.nested_attributes_options[association_name]
+ options = nested_attributes_options[association_name]
if attributes.respond_to?(:permitted?)
attributes = attributes.to_h
end
@@ -452,7 +452,7 @@ module ActiveRecord
# { id: '2', _destroy: true }
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
- options = self.nested_attributes_options[association_name]
+ options = nested_attributes_options[association_name]
if attributes_collection.respond_to?(:permitted?)
attributes_collection = attributes_collection.to_h
end
@@ -562,7 +562,7 @@ module ActiveRecord
def call_reject_if(association_name, attributes)
return false if will_be_destroyed?(association_name, attributes)
- case callback = self.nested_attributes_options[association_name][:reject_if]
+ case callback = nested_attributes_options[association_name][:reject_if]
when Symbol
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
when Proc
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 2bb7ed6d5e..26966f9433 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -4,7 +4,7 @@ module ActiveRecord
[]
end
- def delete_all(_conditions = nil)
+ def delete_all
0
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 8e13ee3564..7ceb7d1a55 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -148,6 +148,8 @@ module ActiveRecord
#
# Attributes marked as readonly are silently ignored if the record is
# being updated.
+ #
+ # Unless an error is raised, returns true.
def save!(*args)
create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self))
end
@@ -181,7 +183,11 @@ module ActiveRecord
_raise_readonly_record_error if readonly?
destroy_associations
self.class.connection.add_transaction_record(self)
- destroy_row if persisted?
+ @_trigger_destroy_callback = if persisted?
+ destroy_row > 0
+ else
+ true
+ end
@destroyed = true
freeze
end
@@ -334,14 +340,16 @@ module ActiveRecord
self
end
- # Wrapper around #increment that saves the record. This method differs from
- # its non-bang version in that it passes through the attribute setter.
- # Saving is not subjected to validation checks. Returns +true+ if the
- # record could be saved.
- def increment!(attribute, by = 1)
+ # Wrapper around #increment that writes the update to the database.
+ # Only +attribute+ is updated; the record itself is not saved.
+ # This means that any other modified attributes will still be dirty.
+ # Validations and callbacks are skipped. Supports the `touch` option from
+ # +update_counters+, see that for more.
+ # Returns +self+.
+ def increment!(attribute, by = 1, touch: nil)
increment(attribute, by)
change = public_send(attribute) - (attribute_in_database(attribute.to_s) || 0)
- self.class.update_counters(id, attribute => change)
+ self.class.update_counters(id, attribute => change, touch: touch)
clear_attribute_change(attribute) # eww
self
end
@@ -353,12 +361,14 @@ module ActiveRecord
increment(attribute, -by)
end
- # Wrapper around #decrement that saves the record. This method differs from
- # its non-bang version in the sense that it passes through the attribute setter.
- # Saving is not subjected to validation checks. Returns +true+ if the
- # record could be saved.
- def decrement!(attribute, by = 1)
- increment!(attribute, -by)
+ # Wrapper around #decrement that writes the update to the database.
+ # Only +attribute+ is updated; the record itself is not saved.
+ # This means that any other modified attributes will still be dirty.
+ # Validations and callbacks are skipped. Supports the `touch` option from
+ # +update_counters+, see that for more.
+ # Returns +self+.
+ def decrement!(attribute, by = 1, touch: nil)
+ increment!(attribute, -by, touch: touch)
end
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
@@ -388,8 +398,8 @@ module ActiveRecord
# Reloads the record from the database.
#
- # This method finds record by its primary key (which could be assigned manually) and
- # modifies the receiver in-place:
+ # This method finds the record by its primary key (which could be assigned
+ # manually) and modifies the receiver in-place:
#
# account = Account.new
# # => #<Account id: nil, email: nil>
@@ -519,6 +529,7 @@ module ActiveRecord
raise ActiveRecord::StaleObjectError.new(self, "touch")
end
+ @_trigger_update_callback = result
result
else
true
@@ -550,10 +561,13 @@ module ActiveRecord
def _update_record(attribute_names = self.attribute_names)
attributes_values = arel_attributes_with_values_for_update(attribute_names)
if attributes_values.empty?
- 0
+ rows_affected = 0
+ @_trigger_update_callback = true
else
- self.class.unscoped._update_record attributes_values, id, id_in_database
+ rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database
+ @_trigger_update_callback = rows_affected > 0
end
+ rows_affected
end
# Creates a record with values matching those of the instance attributes
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 36689f6559..c4a22398f0 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -9,7 +9,7 @@ module ActiveRecord
delegate :find_each, :find_in_batches, :in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :merge, to: :all
+ :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
delegate :pluck, :ids, to: :all
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 7ce10df6d4..0276d41494 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -82,15 +82,15 @@ module ActiveRecord
if config.active_record.delete(:use_schema_cache_dump)
config.after_initialize do |app|
ActiveSupport.on_load(:active_record) do
- filename = File.join(app.config.paths["db"].first, "schema_cache.dump")
+ filename = File.join(app.config.paths["db"].first, "schema_cache.yml")
if File.file?(filename)
- cache = Marshal.load File.binread filename
+ cache = YAML.load(File.read(filename))
if cache.version == ActiveRecord::Migrator.current_version
- self.connection.schema_cache = cache
- self.connection_pool.schema_cache = cache.dup
+ connection.schema_cache = cache
+ connection_pool.schema_cache = cache.dup
else
- warn "Ignoring db/schema_cache.dump 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 #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
end
end
end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index adb3c6c4e6..8658188623 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -6,10 +6,14 @@ module ActiveRecord
module ControllerRuntime #:nodoc:
extend ActiveSupport::Concern
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
+ # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_internal :db_runtime
+ private
+
def process_action(action, *args)
# We also need to reset the runtime before each action
# because of queries in middleware or in cases we are streaming
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 46235ab922..f3e2df8786 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -77,6 +77,8 @@ db_namespace = namespace :db do
namespace :migrate do
# desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
task redo: [:environment, :load_config] do
+ raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
+
if ENV["VERSION"]
db_namespace["migrate:down"].invoke
db_namespace["migrate:up"].invoke
@@ -91,16 +93,17 @@ db_namespace = namespace :db do
# desc 'Runs the "up" for a given migration VERSION.'
task up: [:environment, :load_config] do
+ raise "VERSION is required" if ENV["VERSION"] && ENV["VERSION"].empty?
+
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- raise "VERSION is required" unless version
ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
db_namespace["_dump"].invoke
end
# desc 'Runs the "down" for a given migration VERSION.'
task down: [:environment, :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
- raise "VERSION is required - To go down one migration, run db:rollback" unless version
ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
db_namespace["_dump"].invoke
end
@@ -110,28 +113,13 @@ db_namespace = namespace :db do
unless ActiveRecord::SchemaMigration.table_exists?
abort "Schema migrations table does not exist yet."
end
- db_list = ActiveRecord::SchemaMigration.normalized_versions
-
- file_list =
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path|
- Dir.foreach(path).map do |file|
- next unless ActiveRecord::Migrator.match_to_migration_filename?(file)
-
- version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file)
- version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
- status = db_list.delete(version) ? "up" : "down"
- [status, version, (name + scope).humanize]
- end.compact
- end
- db_list.map! do |version|
- ["up", version, "********** NO FILE **********"]
- end
# output
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
+ ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name|
puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
@@ -265,19 +253,16 @@ db_namespace = namespace :db do
end
namespace :cache do
- desc "Creates a db/schema_cache.dump file."
+ desc "Creates a db/schema_cache.yml file."
task dump: [:environment, :load_config] do
- con = ActiveRecord::Base.connection
- filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
-
- con.schema_cache.clear!
- con.data_sources.each { |table| con.schema_cache.add(table) }
- open(filename, "wb") { |f| f.write(Marshal.dump(con.schema_cache)) }
+ 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.dump file."
+ desc "Clears a db/schema_cache.yml file."
task clear: [:environment, :load_config] do
- filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml")
rm_f filename, verbose: false
end
end
@@ -291,8 +276,7 @@ db_namespace = namespace :db do
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- if ActiveRecord::Base.connection.supports_migrations? &&
- ActiveRecord::SchemaMigration.table_exists?
+ if ActiveRecord::SchemaMigration.table_exists?
File.open(filename, "a") do |f|
f.puts ActiveRecord::Base.connection.dump_schema_information
f.print "\n"
@@ -312,14 +296,6 @@ db_namespace = namespace :db do
end
namespace :test do
-
- task :deprecated do
- Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
- $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
- "your test schema automatically, see the release notes for details."
- end
- end
-
# desc "Recreate the test database from the current schema"
task load: %w(db:test:purge) do
case ActiveRecord::Base.schema_format
@@ -348,22 +324,6 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"]
end
- # desc "Recreate the test database from a fresh schema"
- task clone: %w(db:test:deprecated environment) do
- case ActiveRecord::Base.schema_format
- when :ruby
- db_namespace["test:clone_schema"].invoke
- when :sql
- db_namespace["test:clone_structure"].invoke
- end
- end
-
- # desc "Recreate the test database from a fresh schema.rb file"
- task clone_schema: %w(db:test:deprecated db:schema:dump db:test:load_schema)
-
- # desc "Recreate the test database from a fresh structure.sql file"
- task clone_structure: %w(db:test:deprecated db:structure:dump db:test:load_structure)
-
# desc "Empty the test database"
task purge: %w(environment load_config check_protected_environments) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"]
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 8ff265bdfa..6274996ab8 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# Attributes listed as readonly will be used to create a new record but update operations will
# ignore these fields.
def attr_readonly(*attributes)
- self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || [])
+ self._attr_readonly = Set.new(attributes.map(&:to_s)) + (_attr_readonly || [])
end
# Returns an array of all the attributes that have been specified as readonly.
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index ef3c3bfae8..24ca8b0be4 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,5 +1,6 @@
require "thread"
require "active_support/core_ext/string/filters"
+require "active_support/deprecation"
module ActiveRecord
# = Active Record Reflection
@@ -171,12 +172,47 @@ module ActiveRecord
JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
- def join_keys(association_klass)
- JoinKeys.new(foreign_key, active_record_primary_key)
+ def join_keys
+ get_join_keys klass
+ end
+
+ # Returns a list of scopes that should be applied for this Reflection
+ # object when querying the database.
+ def scopes
+ scope ? [scope] : []
+ end
+
+ def scope_chain
+ chain.map(&:scopes)
+ end
+ deprecate :scope_chain
+
+ def join_scopes(table, predicate_builder) # :nodoc:
+ if scope
+ [ActiveRecord::Relation.create(klass, table, predicate_builder)
+ .instance_exec(&scope)]
+ else
+ []
+ end
+ end
+
+ def klass_join_scope(table, predicate_builder) # :nodoc:
+ if klass.current_scope
+ klass.current_scope.clone.tap { |scope|
+ scope.joins_values = []
+ }
+ else
+ relation = ActiveRecord::Relation.create(
+ klass,
+ table,
+ predicate_builder,
+ )
+ klass.send(:build_default_scope, relation)
+ end
end
def constraints
- scope_chain.flatten
+ chain.map(&:scopes).flatten
end
def counter_cache_column
@@ -248,6 +284,20 @@ module ActiveRecord
def chain
collect_join_chain
end
+
+ def get_join_keys(association_klass)
+ JoinKeys.new(join_pk(association_klass), join_fk)
+ end
+
+ private
+
+ def join_pk(_)
+ foreign_key
+ end
+
+ def join_fk
+ active_record_primary_key
+ end
end
# Base class for AggregateReflection and AssociationReflection. Objects of
@@ -321,7 +371,7 @@ module ActiveRecord
end
end
- # Holds all the meta-data about an aggregation as it was specified in the
+ # Holds all the metadata about an aggregation as it was specified in the
# Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
def mapping
@@ -330,7 +380,7 @@ module ActiveRecord
end
end
- # Holds all the meta-data about an association as it was specified in the
+ # Holds all the metadata about an association as it was specified in the
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
# Returns the target association's class.
@@ -364,6 +414,17 @@ module ActiveRecord
@constructable = calculate_constructable(macro, options)
@association_scope_cache = {}
@scope_lock = Mutex.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
+ end
end
def association_scope_cache(conn, owner)
@@ -397,6 +458,10 @@ 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
@@ -446,12 +511,6 @@ module ActiveRecord
false
end
- # An array of arrays of scopes. Each item in the outside array corresponds to a reflection
- # in the #chain.
- def scope_chain
- scope ? [[scope]] : [[]]
- end
-
def has_scope?
scope
end
@@ -666,11 +725,6 @@ module ActiveRecord
end
end
- def join_keys(association_klass)
- key = polymorphic? ? association_primary_key(association_klass) : association_primary_key
- JoinKeys.new(key, foreign_key)
- end
-
def join_id_for(owner) # :nodoc:
owner[foreign_key]
end
@@ -680,6 +734,14 @@ module ActiveRecord
def calculate_constructable(macro, options)
!polymorphic?
end
+
+ def join_fk
+ foreign_key
+ end
+
+ def join_pk(klass)
+ polymorphic? ? association_primary_key(klass) : association_primary_key
+ end
end
class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
@@ -694,12 +756,12 @@ module ActiveRecord
end
end
- # Holds all the meta-data about a :through association as it was specified
+ # Holds all the metadata about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AbstractReflection #:nodoc:
attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
- :active_record_primary_key, :type, to: :source_reflection
+ :active_record_primary_key, :type, :get_join_keys, to: :source_reflection
def initialize(delegate_reflection)
@delegate_reflection = delegate_reflection
@@ -781,45 +843,16 @@ module ActiveRecord
through_reflection.clear_association_scope_cache
end
- # Consider the following example:
- #
- # class Person
- # has_many :articles
- # has_many :comment_tags, through: :articles
- # end
- #
- # class Article
- # has_many :comments
- # has_many :comment_tags, through: :comments, source: :tags
- # end
- #
- # class Comment
- # has_many :tags
- # end
- #
- # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags,
- # but only Comment.tags will be represented in the #chain. So this method creates an array
- # of scopes corresponding to the chain.
- def scope_chain
- @scope_chain ||= begin
- scope_chain = source_reflection.scope_chain.map(&:dup)
-
- # Add to it the scope from this reflection (if any)
- scope_chain.first << scope if scope
-
- through_scope_chain = through_reflection.scope_chain.map(&:dup)
+ def scopes
+ source_reflection.scopes + super
+ end
- if options[:source_type]
- type = foreign_type
- source_type = options[:source_type]
- through_scope_chain.first << lambda { |object|
- where(type => source_type)
- }
- end
+ def join_scopes(table, predicate_builder) # :nodoc:
+ source_reflection.join_scopes(table, predicate_builder) + super
+ end
- # Recursively fill out the rest of the array from the through reflection
- scope_chain + through_scope_chain
- end
+ def source_type_scope
+ through_reflection.klass.where(foreign_type => options[:source_type])
end
def has_scope?
@@ -828,10 +861,6 @@ module ActiveRecord
through_reflection.has_scope?
end
- def join_keys(association_klass)
- source_reflection.join_keys(association_klass)
- end
-
# A through association is nested if there would be more than one join table
def nested?
source_reflection.through_reflection? || through_reflection.through_reflection?
@@ -846,6 +875,10 @@ 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
@@ -870,15 +903,13 @@ module ActiveRecord
}
if names.length > 1
- example_options = options.dup
- example_options[:source] = source_reflection_names.first
- ActiveSupport::Deprecation.warn \
- "Ambiguous source reflection for through association. Please " \
- "specify a :source directive on your declaration like:\n" \
- "\n" \
- " class #{active_record.name} < ActiveRecord::Base\n" \
- " #{macro} :#{name}, #{example_options}\n" \
- " end"
+ raise AmbiguousSourceReflectionForThroughAssociation.new(
+ active_record.name,
+ macro,
+ name,
+ options,
+ source_reflection_names
+ )
end
@source_reflection_name = names.first
@@ -925,6 +956,14 @@ module ActiveRecord
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
+ if parent_reflection.nil?
+ reflections = active_record.reflections.keys.map(&:to_sym)
+
+ if reflections.index(through_reflection.name) > reflections.index(name)
+ raise HasManyThroughOrderError.new(active_record.name, self, through_reflection)
+ end
+ end
+
check_validity_of_inverse!
end
@@ -955,7 +994,7 @@ module ActiveRecord
end
end
- protected
+ private
def actual_source_reflection # FIXME: this is a horrible name
source_reflection.send(:actual_source_reflection)
@@ -967,7 +1006,6 @@ module ActiveRecord
def inverse_name; delegate_reflection.send(:inverse_name); end
- private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
@@ -985,6 +1023,24 @@ module ActiveRecord
@previous_reflection = previous_reflection
end
+ def scopes
+ scopes = @previous_reflection.scopes
+ if @previous_reflection.options[:source_type]
+ scopes + [@previous_reflection.source_type_scope]
+ else
+ scopes
+ end
+ end
+
+ def join_scopes(table, predicate_builder) # :nodoc:
+ scopes = @previous_reflection.join_scopes(table, predicate_builder) + super
+ if @previous_reflection.options[:source_type]
+ scopes + [@previous_reflection.source_type_scope]
+ else
+ scopes
+ end
+ end
+
def klass
@reflection.klass
end
@@ -1001,10 +1057,6 @@ module ActiveRecord
@reflection.plural_name
end
- def join_keys(association_klass)
- @reflection.join_keys(association_klass)
- end
-
def type
@reflection.type
end
@@ -1018,6 +1070,10 @@ module ActiveRecord
source_type = @previous_reflection.options[:source_type]
lambda { |object| where(type => source_type) }
end
+
+ def get_join_keys(association_klass)
+ @reflection.get_join_keys(association_klass)
+ end
end
class RuntimeReflection < PolymorphicReflection # :nodoc:
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 6f602e4a23..2d6b21bec5 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -261,10 +261,6 @@ module ActiveRecord
coder.represent_seq(nil, records)
end
- def as_json(options = nil) #:nodoc:
- records.as_json(options)
- end
-
# Returns size of the records.
def size
loaded? ? @records.length : count(:all)
@@ -373,7 +369,7 @@ module ActiveRecord
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
stmt.table(table)
- if joins_values.any?
+ if has_join_values?
@klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
else
stmt.key = arel_attribute(primary_key)
@@ -418,8 +414,7 @@ module ActiveRecord
records.each { |record| record.update(attributes) }
else
if ActiveRecord::Base === id
- id = id.id
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ raise ArgumentError, <<-MSG.squish
You are passing an instance of ActiveRecord::Base to `update`.
Please pass the id of the object by calling `.id`.
MSG
@@ -446,16 +441,8 @@ module ActiveRecord
# ==== Examples
#
# Person.where(age: 0..18).destroy_all
- def destroy_all(conditions = nil)
- if conditions
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1.
- To achieve the same use where(conditions).destroy_all.
- MESSAGE
- where(conditions).destroy_all
- else
- records.each(&:destroy).tap { reset }
- end
+ def destroy_all
+ records.each(&:destroy).tap { reset }
end
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
@@ -503,7 +490,7 @@ module ActiveRecord
#
# Post.limit(100).delete_all
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
- def delete_all(conditions = nil)
+ def delete_all
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
value = get_value(method)
SINGLE_VALUE_METHODS.include?(method) ? value : value.any?
@@ -512,27 +499,19 @@ module ActiveRecord
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
- if conditions
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Passing conditions to delete_all is deprecated and will be removed in Rails 5.1.
- To achieve the same use where(conditions).delete_all.
- MESSAGE
- where(conditions).delete_all
- else
- stmt = Arel::DeleteManager.new
- stmt.from(table)
+ stmt = Arel::DeleteManager.new
+ stmt.from(table)
- if joins_values.any?
- @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
- else
- stmt.wheres = arel.constraints
- end
+ if has_join_values?
+ @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
+ else
+ stmt.wheres = arel.constraints
+ end
- affected = @klass.connection.delete(stmt, "SQL", bound_attributes)
+ affected = @klass.connection.delete(stmt, "SQL", bound_attributes)
- reset
- affected
- end
+ reset
+ affected
end
# Deletes the row with a primary key matching the +id+ argument, using a
@@ -630,15 +609,6 @@ module ActiveRecord
includes_values & joins_values
end
- # {#uniq}[rdoc-ref:QueryMethods#uniq] and
- # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated.
- # #uniq_value delegates to #distinct_value to maintain backwards compatibility.
- # Use #distinct_value instead.
- def uniq_value
- distinct_value
- end
- deprecate uniq_value: :distinct_value
-
# Compares two relations for equality.
def ==(other)
case other
@@ -680,6 +650,10 @@ module ActiveRecord
private
+ def has_join_values?
+ joins_values.any? || left_outer_joins_values.any?
+ end
+
def exec_queries(&block)
@records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 4b2987ac6d..76031515fd 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -260,7 +260,7 @@ module ActiveRecord
end
def act_on_ignored_order(error_on_ignore)
- raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order : error_on_ignore)
+ raise_error = (error_on_ignore.nil? ? klass.error_on_ignored_order : error_on_ignore)
if raise_error
raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 827688a663..9cabd1af13 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -37,11 +37,8 @@ module ActiveRecord
# Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
# between databases. In invalid cases, an error from the database is thrown.
def count(column_name = nil)
- if block_given?
- to_a.count { |*block_args| yield(*block_args) }
- else
- calculate(:count, column_name)
- end
+ return super() if block_given?
+ calculate(:count, column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
@@ -75,8 +72,8 @@ module ActiveRecord
# #calculate for examples with options.
#
# Person.sum(:age) # => 4562
- def sum(column_name = nil, &block)
- return super(&block) if block_given?
+ def sum(column_name = nil)
+ return super() if block_given?
calculate(:sum, column_name)
end
@@ -193,7 +190,7 @@ module ActiveRecord
# If #count is used with #distinct (i.e. `relation.distinct.count`) it is
# considered distinct.
- distinct = self.distinct_value
+ distinct = distinct_value
if operation == "count"
column_name ||= select_for_count
@@ -232,7 +229,7 @@ module ActiveRecord
query_builder = build_count_subquery(spawn, column_name, distinct)
else
# PostgreSQL doesn't like ORDER BY when there are no GROUP BY
- relation = unscope(:order)
+ relation = unscope(:order).distinct!(false)
column = aggregate_column(column_name)
@@ -282,7 +279,7 @@ module ActiveRecord
operation,
distinct).as(aggregate_alias)
]
- select_values += select_values unless having_clause.empty?
+ select_values += self.select_values unless having_clause.empty?
select_values.concat group_columns.map { |aliaz, field|
if field.respond_to?(:as)
@@ -292,7 +289,7 @@ module ActiveRecord
end
}
- relation = except(:group)
+ relation = except(:group).distinct!(false)
relation.group_values = group_fields
relation.select_values = select_values
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 4b9310b225..50378f9d99 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -15,7 +15,10 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- const_set klass.name.gsub("::".freeze, "_".freeze), delegate
+ mangled_name = klass.name.gsub("::".freeze, "_".freeze)
+ const_set mangled_name, delegate
+ private_constant mangled_name
+
cache[klass] = delegate
end
end
@@ -33,8 +36,9 @@ 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, :collect, :map, :each, :all?, :include?, :to_ary, :join,
+ delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
+ :to_sentence, :to_formatted_s, :as_json,
:shuffle, :split, :index, to: :records
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
@@ -78,7 +82,7 @@ module ActiveRecord
end
end
- protected
+ private
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
@@ -110,7 +114,7 @@ module ActiveRecord
arel.respond_to?(method, include_private)
end
- protected
+ private
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 55ded4c6d0..5d24f5f5ca 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -17,8 +17,8 @@ module ActiveRecord
# 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'd need to provide an explicit QueryMethods#order
- # option if you want the results are sorted.
+ # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order
+ # option if you want the results to be sorted.
#
# ==== Find with lock
#
@@ -76,7 +76,7 @@ module ActiveRecord
# Post.find_by "published_at < ?", 2.weeks.ago
def find_by(arg, *args)
where(arg, *args).take
- rescue RangeError
+ rescue ::RangeError
nil
end
@@ -84,7 +84,7 @@ module ActiveRecord
# an ActiveRecord::RecordNotFound error.
def find_by!(arg, *args)
where(arg, *args).take!
- rescue RangeError
+ rescue ::RangeError
raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
@klass.name)
end
@@ -147,19 +147,11 @@ module ActiveRecord
def last(limit = nil)
return find_last(limit) if loaded? || limit_value
- result = limit(limit || 1)
+ result = limit(limit)
result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key
result = result.reverse_order!
limit ? result.reverse : result.first
- rescue ActiveRecord::IrreversibleOrderError
- ActiveSupport::Deprecation.warn(<<-WARNING.squish)
- Finding a last element by loading the relation when SQL ORDER
- can not be reversed is deprecated.
- Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case.
- Please call `to_a.last` if you still want to load the relation.
- WARNING
- find_last(limit)
end
# Same as #last but raises ActiveRecord::RecordNotFound if no record
@@ -309,8 +301,7 @@ module ActiveRecord
# Person.exists?
def exists?(conditions = :none)
if Base === conditions
- conditions = conditions.id
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ raise ArgumentError, <<-MSG.squish
You are passing an instance of ActiveRecord::Base to `exists?`.
Please pass the id of the object by calling `.id`.
MSG
@@ -333,7 +324,7 @@ module ActiveRecord
end
connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false
- rescue RangeError
+ rescue ::RangeError
false
end
@@ -345,7 +336,7 @@ module ActiveRecord
# of results obtained should be provided in the +result_size+ argument and
# the expected number of results should be provided in the +expected_size+
# argument.
- def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil) # :nodoc:
+ def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key) # :nodoc:
conditions = arel.where_sql(@klass.arel_engine)
conditions = " [#{conditions}]" if conditions
name = @klass.name
@@ -355,10 +346,10 @@ module ActiveRecord
error << " with#{conditions}" if conditions
raise RecordNotFound.new(error, name)
elsif Array(ids).size == 1
- error = "Couldn't find #{name} with '#{primary_key}'=#{ids}#{conditions}"
- raise RecordNotFound.new(error, name, primary_key, ids)
+ error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
+ raise RecordNotFound.new(error, name, key, ids)
else
- error = "Couldn't find all #{name.pluralize} with '#{primary_key}': "
+ error = "Couldn't find all #{name.pluralize} with '#{key}': "
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
raise RecordNotFound.new(error, name, primary_key, ids)
@@ -439,8 +430,6 @@ module ActiveRecord
reflections.none?(&:collection?)
end
- protected
-
def find_with_ids(*ids)
raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
@@ -458,14 +447,13 @@ module ActiveRecord
else
find_some(ids)
end
- rescue RangeError
+ rescue ::RangeError
raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
end
def find_one(id)
if ActiveRecord::Base === id
- id = id.id
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ raise ArgumentError, <<-MSG.squish
You are passing an instance of ActiveRecord::Base to `find`.
Please pass the id of the object by calling `.id`.
MSG
@@ -548,8 +536,12 @@ module ActiveRecord
self
end
- relation = relation.offset(offset_index + index) unless index.zero?
- relation.limit(limit).to_a
+ if limit_value.nil? || index < limit_value
+ relation = relation.offset(offset_index + index) unless index.zero?
+ relation.limit(limit).to_a
+ else
+ []
+ end
end
end
@@ -572,8 +564,6 @@ module ActiveRecord
end
end
- private
-
def find_last(limit)
limit ? records.last(limit) : records.last
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 780a1ee422..fca914aedd 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -4,7 +4,6 @@ module ActiveRecord
require "active_record/relation/predicate_builder/association_query_handler"
require "active_record/relation/predicate_builder/base_handler"
require "active_record/relation/predicate_builder/basic_object_handler"
- require "active_record/relation/predicate_builder/class_handler"
require "active_record/relation/predicate_builder/polymorphic_array_handler"
require "active_record/relation/predicate_builder/range_handler"
require "active_record/relation/predicate_builder/relation_handler"
@@ -16,7 +15,6 @@ module ActiveRecord
@handlers = []
register_handler(BasicObject, BasicObjectHandler.new)
- register_handler(Class, ClassHandler.new(self))
register_handler(Base, BaseHandler.new(self))
register_handler(Range, RangeHandler.new)
register_handler(RangeHandler::RangeWithBinds, RangeHandler.new)
@@ -66,6 +64,8 @@ module ActiveRecord
handler_for(value).call(attribute, 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 :table
@@ -87,14 +87,19 @@ module ActiveRecord
binds = []
attributes.each do |column_name, value|
+ binds.concat(value.bound_attributes) if value.is_a?(Relation)
case
when value.is_a?(Hash) && !table.has_column?(column_name)
attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value)
result[column_name] = attrs
binds += bvs
- next
- when value.is_a?(Relation)
- binds += value.bound_attributes
+ when table.associated_with?(column_name)
+ # Find the foreign key when using queries such as:
+ # Post.where(author: author)
+ #
+ # For polymorphic relationships, find the foreign key and type:
+ # PriceEstimate.where(estimate_of: treasure)
+ result[column_name] = AssociationQueryHandler.value_for(table, column_name, value)
when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype)
first = value.begin
last = value.end
@@ -114,15 +119,6 @@ module ActiveRecord
binds << build_bind_param(column_name, value)
end
end
-
- # Find the foreign key when using queries such as:
- # Post.where(author: author)
- #
- # For polymorphic relationships, find the foreign key and type:
- # PriceEstimate.where(estimate_of: treasure)
- if table.associated_with?(column_name)
- result[column_name] = AssociationQueryHandler.value_for(table, column_name, value)
- end
end
[result, binds]
@@ -155,7 +151,6 @@ module ActiveRecord
end
def can_be_bound?(column_name, value)
- return if table.associated_with?(column_name)
case value
when Array, Range
table.type(column_name).respond_to?(:subtype)
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 6400caba06..88b6c37d43 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -29,6 +29,8 @@ module ActiveRecord
array_predicates.inject { |composite, predicate| composite.or(predicate) }
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 :predicate_builder
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
index 7e20cb2c63..29860ec677 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -28,6 +28,8 @@ module ActiveRecord
predicate_builder.build_from_hash(queries)
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 :predicate_builder
@@ -68,9 +70,6 @@ module ActiveRecord
case value
when Relation
value.klass.base_class
- when Array
- val = value.compact.first
- val.class.base_class if val.is_a?(Base)
when Base
value.class.base_class
end
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 65c5159704..3bb1037885 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -9,6 +9,8 @@ module ActiveRecord
predicate_builder.build(attribute, value.id)
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 :predicate_builder
diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
deleted file mode 100644
index 0a6574fcf1..0000000000
--- a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module ActiveRecord
- class PredicateBuilder
- class ClassHandler # :nodoc:
- def initialize(predicate_builder)
- @predicate_builder = predicate_builder
- end
-
- def call(attribute, value)
- print_deprecation_warning
- predicate_builder.build(attribute, value.name)
- end
-
- protected
-
- attr_reader :predicate_builder
-
- private
-
- def print_deprecation_warning
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing a class as a value in an Active Record query is deprecated and
- will be removed. Pass a string instead.
- MSG
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
index 0c7f92b3d0..335124c952 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
@@ -21,6 +21,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 :predicate_builder
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 5f5d8ceea3..1178dec706 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -3,7 +3,6 @@ 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"
-require "active_support/core_ext/string/filters"
module ActiveRecord
module QueryMethods
@@ -76,7 +75,7 @@ module ActiveRecord
end
def bound_attributes
- if limit_value && !string_containing_comma?(limit_value)
+ if limit_value
limit_bind = Attribute.with_cast_value(
"LIMIT".freeze,
connection.sanitize_limit(limit_value),
@@ -243,9 +242,7 @@ module ActiveRecord
def select(*fields)
if block_given?
if fields.any?
- ActiveSupport::Deprecation.warn(<<-WARNING.squish)
- When select is called with a block, it ignores other arguments. This behavior is now deprecated and will result in an ArgumentError in Rails 5.1. You can safely remove the arguments to resolve the deprecation warning because they do not have any effect on the output of the call to the select method with a block.
- WARNING
+ raise ArgumentError, "`select' with block doesn't take arguments."
end
return super()
@@ -659,7 +656,7 @@ module ActiveRecord
end
self.where_clause = self.where_clause.or(other.where_clause)
- self.having_clause = self.having_clause.or(other.having_clause)
+ self.having_clause = having_clause.or(other.having_clause)
self
end
@@ -690,13 +687,6 @@ module ActiveRecord
end
def limit!(value) # :nodoc:
- if string_containing_comma?(value)
- # Remove `string_containing_comma?` when removing this deprecation
- ActiveSupport::Deprecation.warn(<<-WARNING.squish)
- Passing a string to limit in the form "1,2" is deprecated and will be
- removed in Rails 5.1. Please call `offset` explicitly instead.
- WARNING
- end
self.limit_value = value
self
end
@@ -848,16 +838,12 @@ module ActiveRecord
def distinct(value = true)
spawn.distinct!(value)
end
- alias uniq distinct
- deprecate uniq: :distinct
# Like #distinct, but modifies relation in place.
def distinct!(value = true) # :nodoc:
self.distinct_value = value
self
end
- alias uniq! distinct!
- deprecate uniq!: :distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -958,13 +944,7 @@ module ActiveRecord
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
- if limit_value
- if string_containing_comma?(limit_value)
- arel.take(connection.sanitize_limit(limit_value))
- else
- arel.take(Arel::Nodes::BindParam.new)
- end
- end
+ arel.take(Arel::Nodes::BindParam.new) if limit_value
arel.skip(Arel::Nodes::BindParam.new) if offset_value
arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
@@ -1150,7 +1130,12 @@ module ActiveRecord
arel_attribute(arg).asc
when Hash
arg.map { |field, dir|
- arel_attribute(field).send(dir.downcase)
+ case field
+ when Arel::Nodes::SqlLiteral
+ field.send(dir.downcase)
+ else
+ arel_attribute(field).send(dir.downcase)
+ end
}
else
arg
@@ -1192,10 +1177,6 @@ module ActiveRecord
end
alias having_clause_factory where_clause_factory
- def string_containing_comma?(value)
- ::String === value && value.include?(",")
- end
-
def default_value_for(name)
case name
when :create_with
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 190e339ea8..ada89b5ec3 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -66,7 +66,7 @@ module ActiveRecord
private
- def relation_with(values) # :nodoc:
+ def relation_with(values)
result = Relation.create(klass, table, predicate_builder, values)
result.extend(*extending_values) if extending_values.any?
result
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 402f8acfd1..119910ee79 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -25,10 +25,7 @@ module ActiveRecord
end
def except(*columns)
- WhereClause.new(
- predicates_except(columns),
- binds_except(columns),
- )
+ WhereClause.new(*except_predicates_and_binds(columns))
end
def or(other)
@@ -84,6 +81,8 @@ module ActiveRecord
@empty ||= new([], [])
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 :predicates
@@ -132,20 +131,35 @@ module ActiveRecord
end
end
- def predicates_except(columns)
- 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)
- columns.include?(subrelation.name.to_s)
+ def except_predicates_and_binds(columns)
+ except_binds = []
+ binds_index = 0
+
+ predicates = self.predicates.reject do |node|
+ except = \
+ 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
+ binds_contains = node.grep(Arel::Nodes::BindParam).size
+ subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
+ columns.include?(subrelation.name.to_s)
+ end
+
+ if except && binds_contains > 0
+ (binds_index...(binds_index + binds_contains)).each do |i|
+ except_binds[i] = true
+ end
end
+
+ binds_index += binds_contains if binds_contains
+
+ except
end
- end
- def binds_except(columns)
- binds.reject do |attr|
- columns.include?(attr.name)
+ binds = self.binds.reject.with_index do |_, i|
+ except_binds[i]
end
+
+ [predicates, binds]
end
def predicates_with_wrapped_sql_literals
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 1e7deeffad..04bee73e8f 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -15,9 +15,12 @@ module ActiveRecord
attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
attributes.stringify_keys!
- attributes, binds = predicate_builder.create_binds(attributes)
-
- parts = predicate_builder.build_from_hash(attributes)
+ if perform_case_sensitive?(options = other.last)
+ parts, binds = build_for_case_sensitive(attributes, options)
+ else
+ attributes, binds = predicate_builder.create_binds(attributes)
+ parts = predicate_builder.build_from_hash(attributes)
+ end
when Arel::Nodes::Node
parts = [opts]
else
@@ -27,9 +30,48 @@ module ActiveRecord
WhereClause.new(parts, binds || [])
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 :klass, :predicate_builder
+
+ private
+
+ def perform_case_sensitive?(options)
+ options && options.key?(:case_sensitive)
+ end
+
+ def build_for_case_sensitive(attributes, options)
+ parts, binds = [], []
+ table = klass.arel_table
+
+ attributes.each do |attribute, value|
+ if reflection = klass._reflect_on_association(attribute)
+ attribute = reflection.foreign_key.to_s
+ value = value[reflection.klass.primary_key] unless value.nil?
+ end
+
+ if value.nil?
+ parts << table[attribute].eq(value)
+ else
+ column = klass.column_for_attribute(attribute)
+
+ binds << predicate_builder.send(:build_bind_param, attribute, value)
+ value = Arel::Nodes::BindParam.new
+
+ predicate = if options[:case_sensitive]
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
+ else
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
+ end
+
+ parts << predicate
+ end
+ end
+
+ [parts, binds]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 9ed70a9c2b..26b1d48e9e 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -41,10 +41,15 @@ module ActiveRecord
@column_types = column_types
end
+ # Returns the number of elements in the rows array.
def length
@rows.length
end
+ # Calls the given block once for each element in row collection, passing
+ # row as parameter.
+ #
+ # Returns an +Enumerator+ if no block is given.
def each
if block_given?
hash_rows.each { |row| yield row }
@@ -53,6 +58,7 @@ module ActiveRecord
end
end
+ # Returns an array of hashes representing each row record.
def to_hash
hash_rows
end
@@ -60,11 +66,12 @@ module ActiveRecord
alias :map! :map
alias :collect! :map
- # Returns true if there are no records.
+ # Returns true if there are no records, otherwise false.
def empty?
rows.empty?
end
+ # Returns an array of hashes representing each row record.
def to_ary
hash_rows
end
@@ -73,11 +80,15 @@ module ActiveRecord
hash_rows[idx]
end
+ # Returns the first record from the rows collection.
+ # If the rows collection is empty, returns +nil+.
def first
return nil if @rows.empty?
Hash[@columns.zip(@rows.first)]
end
+ # Returns the last record from the rows collection.
+ # If the rows collection is empty, returns +nil+.
def last
return nil if @rows.empty?
Hash[@columns.zip(@rows.last)]
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 3d52dc44cf..64bda1539c 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -1,10 +1,9 @@
-
module ActiveRecord
module Sanitization
extend ActiveSupport::Concern
module ClassMethods
- protected
+ private
# Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
@@ -20,7 +19,7 @@ module ActiveRecord
#
# sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
# # => "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition)
+ def sanitize_sql_for_conditions(condition) # :doc:
return nil if condition.blank?
case condition
@@ -46,7 +45,7 @@ module ActiveRecord
#
# 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 = self.table_name)
+ 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)
@@ -62,7 +61,7 @@ module ActiveRecord
#
# sanitize_sql_for_order("id ASC")
# # => "id ASC"
- def sanitize_sql_for_order(condition)
+ def sanitize_sql_for_order(condition) # :doc:
if condition.is_a?(Array) && condition.first.to_s.include?("?")
sanitize_sql_array(condition)
else
@@ -85,7 +84,7 @@ module ActiveRecord
#
# { address: Address.new("813 abc st.", "chicago") }
# # => { address_street: "813 abc st.", address_city: "chicago" }
- def expand_hash_conditions_for_aggregates(attrs)
+ def expand_hash_conditions_for_aggregates(attrs) # :doc:
expanded_attrs = {}
attrs.each do |attr, value|
if aggregation = reflect_on_aggregation(attr.to_sym)
@@ -108,7 +107,7 @@ module ActiveRecord
#
# 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)
+ 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)
@@ -130,7 +129,7 @@ module ActiveRecord
#
# sanitize_sql_like("snake_cased_string", "!")
# # => "snake!_cased!_string"
- def sanitize_sql_like(string, escape_character = "\\")
+ def sanitize_sql_like(string, escape_character = "\\") # :doc:
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
end
@@ -146,7 +145,7 @@ module ActiveRecord
#
# sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
# # => "name='foo''bar' and group_id='4'"
- def sanitize_sql_array(ary)
+ 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)
@@ -159,7 +158,7 @@ module ActiveRecord
end
end
- def replace_bind_variables(statement, values) # :nodoc:
+ def replace_bind_variables(statement, values)
raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
bound = values.dup
c = connection
@@ -168,7 +167,7 @@ module ActiveRecord
end
end
- def replace_bind_variable(value, c = connection) # :nodoc:
+ def replace_bind_variable(value, c = connection)
if ActiveRecord::Relation === value
value.to_sql
else
@@ -176,7 +175,7 @@ module ActiveRecord
end
end
- def replace_named_bind_variables(statement, bind_vars) # :nodoc:
+ def replace_named_bind_variables(statement, bind_vars)
statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
if $1 == ":" # skip postgresql casts
match # return the whole match
@@ -188,7 +187,7 @@ module ActiveRecord
end
end
- def quote_bound_value(value, c = connection) # :nodoc:
+ def quote_bound_value(value, c = connection)
if value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
@@ -200,16 +199,16 @@ module ActiveRecord
end
end
- def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc:
+ def raise_if_bind_arity_mismatch(statement, expected, provided)
unless expected == provided
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
end
end
end
- # TODO: Deprecate this
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 99e54a8b24..5104427339 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -48,7 +48,7 @@ module ActiveRecord
instance_eval(&block)
if info[:version].present?
- initialize_schema_migrations_table
+ ActiveRecord::SchemaMigration.create_table
connection.assume_migrated_upto_version(info[:version], migrations_paths)
end
@@ -61,7 +61,7 @@ module ActiveRecord
#
# ActiveRecord::Schema.new.migrations_paths
# # => ["db/migrate"] # Rails migration path by default.
- def migrations_paths # :nodoc:
+ def migrations_paths
ActiveRecord::Migrator.migrations_paths
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 12289511b7..2bbfd01698 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -85,7 +85,7 @@ HEADER
end
def tables(stream)
- sorted_tables = @connection.data_sources.sort - @connection.views
+ sorted_tables = @connection.tables.sort
sorted_tables.each do |table_name|
table(table_name, stream) unless ignored?(table_name)
@@ -188,7 +188,7 @@ HEADER
index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present?
index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present?
index_parts << "where: #{index.where.inspect}" if index.where
- index_parts << "using: #{index.using.inspect}" if index.using
+ index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
index_parts << "type: #{index.type.inspect}" if index.type
index_parts << "comment: #{index.comment.inspect}" if index.comment
index_parts
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 99b23e5593..f59737afb0 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def table_exists?
- ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) }
+ connection.table_exists?(table_name)
end
def create_table
@@ -39,7 +39,11 @@ module ActiveRecord
end
def normalized_versions
- pluck(:version).map { |v| normalize_migration_number v }
+ all_versions.map { |v| normalize_migration_number v }
+ end
+
+ def all_versions
+ order(:version).pluck(:version)
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 9d8253faa3..2daa48859a 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -44,7 +44,7 @@ module ActiveRecord
self.current_scope = nil
end
- protected
+ private
# Use this macro in your model to set a default scope for all operations on
# the model.
@@ -87,7 +87,7 @@ module ActiveRecord
# # Should return a scope, you can call 'super' here etc.
# end
# end
- def default_scope(scope = nil)
+ def default_scope(scope = nil) # :doc:
scope = Proc.new if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
@@ -101,7 +101,7 @@ module ActiveRecord
self.default_scopes += [scope]
end
- def build_default_scope(base_rel = nil) # :nodoc:
+ def build_default_scope(base_rel = nil)
return if abstract_class?
if default_scope_override.nil?
@@ -122,18 +122,18 @@ module ActiveRecord
end
end
- def ignore_default_scope? # :nodoc:
+ def ignore_default_scope?
ScopeRegistry.value_for(:ignore_default_scope, base_class)
end
- def ignore_default_scope=(ignore) # :nodoc:
+ def ignore_default_scope=(ignore)
ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore)
end
# The ignore_default_scope flag is used to prevent an infinite recursion
# situation where a default scope references a scope which has a default
# scope which references a scope...
- def evaluate_default_scope # :nodoc:
+ def evaluate_default_scope
return if ignore_default_scope?
begin
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 6af84c1266..27cdf8cb7e 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -171,14 +171,14 @@ module ActiveRecord
end
end
- protected
+ private
- def valid_scope_name?(name)
- if respond_to?(name, true) && logger
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
+ def valid_scope_name?(name)
+ if respond_to?(name, true) && logger
+ logger.warn "Creating scope :#{name}. " \
+ "Overwriting existing method #{self.name}.#{name}."
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index 7606961e2e..115799cc20 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -27,7 +27,7 @@ module ActiveRecord
# Load securerandom only when has_secure_token is used.
require "active_support/core_ext/securerandom"
define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token }
- before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?") }
+ before_create { send("#{attribute}=", self.class.generate_unique_secure_token) unless send("#{attribute}?") }
end
def generate_unique_secure_token
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 5a408e7b8e..db2bd0b55e 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -9,7 +9,7 @@ module ActiveRecord #:nodoc:
end
def serializable_hash(options = nil)
- options = options.try(:clone) || {}
+ options = options.try(:dup) || {}
options[:except] = Array(options[:except]).map(&:to_s)
options[:except] |= Array(self.class.inheritance_column)
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 066573192e..006afe7495 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -78,7 +78,7 @@ module ActiveRecord
module ClassMethods
def store(store_attribute, options = {})
- serialize store_attribute, IndifferentCoder.new(options[:coder])
+ serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end
@@ -121,18 +121,17 @@ module ActiveRecord
end
end
- protected
- def read_store_attribute(store_attribute, key)
+ private
+ def read_store_attribute(store_attribute, key) # :doc:
accessor = store_accessor_for(store_attribute)
accessor.read(self, store_attribute, key)
end
- def write_store_attribute(store_attribute, key, value)
+ def write_store_attribute(store_attribute, key, value) # :doc:
accessor = store_accessor_for(store_attribute)
accessor.write(self, store_attribute, key, value)
end
- private
def store_accessor_for(store_attribute)
type_for_attribute(store_attribute.to_s).accessor
end
@@ -178,12 +177,12 @@ module ActiveRecord
end
class IndifferentCoder # :nodoc:
- def initialize(coder_or_class_name)
+ def initialize(attr_name, coder_or_class_name)
@coder =
if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
coder_or_class_name
else
- ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
+ ActiveRecord::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object)
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 58184f3872..71efc1829a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -44,7 +44,7 @@ module ActiveRecord
end
def associated_table(table_name)
- association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize)
+ association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize)
if !association && table_name == arel_table.name
return self
@@ -64,6 +64,8 @@ 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
attr_reader :klass, :arel_table, :association
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index c6204ac36f..46fa8a70a3 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,5 +1,3 @@
-require "active_support/core_ext/string/filters"
-
module ActiveRecord
module Tasks # :nodoc:
class DatabaseAlreadyExists < StandardError; end # :nodoc:
@@ -35,6 +33,16 @@ module ActiveRecord
#
# DatabaseTasks.create_current('production')
module DatabaseTasks
+ ##
+ # :singleton-method:
+ # Extra flags passed to database CLI tool (mysqldump/pg_dump) when calling db:structure:dump
+ mattr_accessor :structure_dump_flags, instance_accessor: false
+
+ ##
+ # :singleton-method:
+ # Extra flags passed to database CLI tool when calling db:structure:load
+ mattr_accessor :structure_load_flags, instance_accessor: false
+
extend self
attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
@@ -154,9 +162,11 @@ module ActiveRecord
end
def migrate
+ raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
+
verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- scope = ENV["SCOPE"]
+ scope = ENV["SCOPE"]
verbose_was, Migration.verbose = Migration.verbose, verbose
Migrator.migrate(migrations_paths, version) do |migration|
scope.blank? || scope == migration.scope
@@ -204,13 +214,13 @@ module ActiveRecord
def structure_dump(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
- class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename)
+ class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename, structure_dump_flags)
end
def structure_load(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
- class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename)
+ 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:
@@ -231,14 +241,6 @@ module ActiveRecord
ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
end
- def load_schema_for(*args)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- This method was renamed to `#load_schema` and will be removed in the future.
- Use `#load_schema` instead.
- MSG
- load_schema(*args)
- end
-
def schema_file(format = ActiveRecord::Base.schema_format)
case format
when :ruby
@@ -267,12 +269,22 @@ module ActiveRecord
if seed_loader
seed_loader.load_seed
else
- raise "You tried to load seed data, but no seed loader is specified. Please specify seed " +
- "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" +
+ raise "You tried to load seed data, but no seed loader is specified. Please specify seed " \
+ "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" \
"Seed loader should respond to load_seed method"
end
end
+ # Dumps the schema cache in YAML format for the connection into the file
+ #
+ # ==== Examples:
+ # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, "tmp/schema_dump.yaml")
+ def dump_schema_cache(conn, filename)
+ conn.schema_cache.clear!
+ conn.data_sources.each { |table| conn.schema_cache.add(table) }
+ open(filename, "wb") { |f| f.write(YAML.dump(conn.schema_cache)) }
+ end
+
private
def class_for_adapter(adapter)
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 5cdb3d53f6..920830b9cf 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -53,21 +53,23 @@ module ActiveRecord
connection.collation
end
- def structure_dump(filename)
+ def structure_dump(filename, extra_flags)
args = prepare_command_options
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["--routines"])
args.concat(["--skip-comments"])
+ args.concat(Array(extra_flags)) if extra_flags
args.concat(["#{configuration['database']}"])
run_cmd("mysqldump", args, "dumping")
end
- def structure_load(filename)
+ def structure_load(filename, extra_flags)
args = prepare_command_options
args.concat(["--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
args.concat(["--database", "#{configuration['database']}"])
+ args.concat(Array(extra_flags)) if extra_flags
run_cmd("mysql", args, "loading")
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 4e9897f7b0..f1af90c1e8 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -1,8 +1,11 @@
+require "tempfile"
+
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
DEFAULT_ENCODING = ENV["CHARSET"] || "utf8"
ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze
+ SQL_COMMENT_BEGIN = "--".freeze
delegate :connection, :establish_connection, :clear_active_connections!,
to: ActiveRecord::Base
@@ -43,7 +46,7 @@ module ActiveRecord
create true
end
- def structure_dump(filename)
+ def structure_dump(filename, extra_flags)
set_psql_env
search_path = \
@@ -57,6 +60,7 @@ module ActiveRecord
end
args = ["-s", "-x", "-O", "-f", filename]
+ args.concat(Array(extra_flags)) if extra_flags
unless search_path.blank?
args += search_path.split(",").map do |part|
"--schema=#{part.strip}"
@@ -64,12 +68,15 @@ module ActiveRecord
end
args << configuration["database"]
run_cmd("pg_dump", args, "dumping")
+ remove_sql_header_comments(filename)
File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
- def structure_load(filename)
+ def structure_load(filename, extra_flags)
set_psql_env
- args = [ "-v", ON_ERROR_STOP_1, "-q", "-f", filename, configuration["database"] ]
+ args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename]
+ args.concat(Array(extra_flags)) if extra_flags
+ args << configuration["database"]
run_cmd("psql", args, "loading")
end
@@ -107,6 +114,22 @@ module ActiveRecord
msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
msg
end
+
+ def remove_sql_header_comments(filename)
+ removing_comments = true
+ tempfile = Tempfile.open("uncommented_structure.sql")
+ begin
+ File.foreach(filename) do |line|
+ unless removing_comments && (line.start_with?(SQL_COMMENT_BEGIN) || line.blank?)
+ tempfile << line
+ removing_comments = false
+ end
+ end
+ ensure
+ tempfile.close
+ end
+ FileUtils.mv(tempfile.path, filename)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index 31f1b7efd4..1f756c2979 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -21,7 +21,7 @@ module ActiveRecord
FileUtils.rm(file)
rescue Errno::ENOENT => error
- raise NoDatabaseError.new(error.message, error)
+ raise NoDatabaseError.new(error.message)
end
def purge
@@ -35,14 +35,16 @@ module ActiveRecord
connection.encoding
end
- def structure_dump(filename)
+ def structure_dump(filename, extra_flags)
dbfile = configuration["database"]
- `sqlite3 #{dbfile} .schema > #{filename}`
+ flags = extra_flags.join(" ") if extra_flags
+ `sqlite3 #{flags} #{dbfile} .schema > #{filename}`
end
- def structure_load(filename)
+ def structure_load(filename, extra_flags)
dbfile = configuration["database"]
- `sqlite3 #{dbfile} < "#{filename}"`
+ flags = extra_flags.join(" ") if extra_flags
+ `sqlite3 #{flags} #{dbfile} < "#{filename}"`
end
private
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 63100e38a1..09d8d1cdd4 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module ActiveRecord
# = Active Record \Timestamp
#
@@ -51,15 +52,41 @@ module ActiveRecord
clear_timestamp_attributes
end
+ class_methods do
+ 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
+
+ def timestamp_attributes_for_create
+ ["created_at", "created_on"]
+ end
+
+ 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
def _create_record
if record_timestamps
current_time = current_time_from_proper_timezone
- all_timestamp_attributes.each do |column|
- column = column.to_s
- if has_attribute?(column) && !attribute_present?(column)
+ all_timestamp_attributes_in_model.each do |column|
+ if !attribute_present?(column)
write_attribute(column, current_time)
end
end
@@ -73,7 +100,6 @@ module ActiveRecord
current_time = current_time_from_proper_timezone
timestamp_attributes_for_update_in_model.each do |column|
- column = column.to_s
next if will_save_change_to_attribute?(column)
write_attribute(column, current_time)
end
@@ -86,30 +112,22 @@ module ActiveRecord
end
def timestamp_attributes_for_create_in_model
- timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) }
+ self.class.send(:timestamp_attributes_for_create_in_model)
end
def timestamp_attributes_for_update_in_model
- timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) }
+ self.class.send(:timestamp_attributes_for_update_in_model)
end
def all_timestamp_attributes_in_model
- timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
- end
-
- def timestamp_attributes_for_update
- [:updated_at, :updated_on]
- end
-
- def timestamp_attributes_for_create
- [:created_at, :created_on]
+ self.class.send(:all_timestamp_attributes_in_model)
end
- def all_timestamp_attributes
- timestamp_attributes_for_create + timestamp_attributes_for_update
+ def current_time_from_proper_timezone
+ self.class.send(:current_time_from_proper_timezone)
end
- def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ def max_updated_column_timestamp(timestamp_names = self.class.send(:timestamp_attributes_for_update))
timestamp_names
.map { |attr| self[attr] }
.compact
@@ -117,10 +135,6 @@ module ActiveRecord
.max
end
- def current_time_from_proper_timezone
- self.class.default_timezone == :utc ? Time.now.utc : Time.now
- end
-
# Clear attributes and changed_attributes
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index af3fc88282..45795fa287 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -11,7 +11,6 @@ module ActiveRecord
:before_commit_without_transaction_enrollment,
:commit_without_transaction_enrollment,
:rollback_without_transaction_enrollment,
- terminator: deprecated_false_terminator,
scope: [:kind, :name]
end
@@ -124,7 +123,7 @@ module ActiveRecord
# # statement will cause a PostgreSQL error, even though the unique
# # constraint is no longer violated:
# Number.create(i: 1)
- # # => "PGError: ERROR: current transaction is aborted, commands
+ # # => "PG::Error: ERROR: current transaction is aborted, commands
# # ignored until end of transaction block"
# end
#
@@ -274,16 +273,6 @@ module ActiveRecord
set_callback(:rollback_without_transaction_enrollment, :after, *args, &block)
end
- def raise_in_transactional_callbacks
- ActiveSupport::Deprecation.warn("ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.")
- true
- end
-
- def raise_in_transactional_callbacks=(value)
- ActiveSupport::Deprecation.warn("ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.")
- value
- end
-
private
def set_options_for_callbacks!(args, enforced_options = {})
@@ -294,7 +283,7 @@ module ActiveRecord
fire_on = Array(options[:on])
assert_valid_transaction_action(fire_on)
options[:if] = Array(options[:if])
- options[:if] << "transaction_include_any_action?(#{fire_on})"
+ options[:if].unshift("transaction_include_any_action?(#{fire_on})")
end
end
@@ -407,10 +396,10 @@ module ActiveRecord
end
end
- protected
+ private
# Save the new record state and id of a record so it can be restored later if a transaction fails.
- def remember_transaction_record_state #:nodoc:
+ def remember_transaction_record_state
@_start_transaction_state[:id] = id
@_start_transaction_state.reverse_merge!(
new_record: @new_record,
@@ -421,18 +410,18 @@ module ActiveRecord
end
# Clear the new record state and id of a record.
- def clear_transaction_record_state #:nodoc:
+ def clear_transaction_record_state
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
end
# Force to clear the transaction record state.
- def force_clear_transaction_record_state #:nodoc:
+ def force_clear_transaction_record_state
@_start_transaction_state.clear
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
- def restore_transaction_record_state(force = false) #:nodoc:
+ def restore_transaction_record_state(force = false)
unless @_start_transaction_state.empty?
transaction_level = (@_start_transaction_state[:level] || 0) - 1
if transaction_level < 1 || force
@@ -450,31 +439,30 @@ module ActiveRecord
end
# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
- def transaction_record_state(state) #:nodoc:
+ def transaction_record_state(state)
@_start_transaction_state[state]
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
- def transaction_include_any_action?(actions) #:nodoc:
+ def transaction_include_any_action?(actions)
actions.any? do |action|
case action
when :create
transaction_record_state(:new_record)
when :destroy
- destroyed?
+ defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback
when :update
- !(transaction_record_state(:new_record) || destroyed?)
+ !(transaction_record_state(:new_record) || destroyed?) &&
+ (defined?(@_trigger_update_callback) && @_trigger_update_callback)
end
end
end
- private
-
- def set_transaction_state(state) # :nodoc:
+ def set_transaction_state(state)
@transaction_state = state
end
- def has_transactional_callbacks? # :nodoc:
+ def has_transactional_callbacks?
!_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty?
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 0b48d2186a..4f632660a8 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -5,7 +5,10 @@ require "active_record/type/internal/timezone"
require "active_record/type/date"
require "active_record/type/date_time"
+require "active_record/type/decimal_without_scale"
require "active_record/type/time"
+require "active_record/type/text"
+require "active_record/type/unsigned_integer"
require "active_record/type/serialized"
require "active_record/type/adapter_specific_registry"
@@ -53,12 +56,9 @@ module ActiveRecord
Binary = ActiveModel::Type::Binary
Boolean = ActiveModel::Type::Boolean
Decimal = ActiveModel::Type::Decimal
- DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
Float = ActiveModel::Type::Float
Integer = ActiveModel::Type::Integer
String = ActiveModel::Type::String
- Text = ActiveModel::Type::Text
- UnsignedInteger = ActiveModel::Type::UnsignedInteger
Value = ActiveModel::Type::Value
register(:big_integer, Type::BigInteger, override: false)
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index d0f9581576..7cc866f7a7 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -50,6 +50,8 @@ 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
@@ -110,6 +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
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..53a5e205da
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module Type
+ class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc:
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s.inspect
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index ac9134bfcb..edbd20a6c1 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,6 +1,8 @@
module ActiveRecord
module Type
class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ undef to_yaml if method_defined?(:to_yaml)
+
include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -43,7 +45,7 @@ module ActiveRecord
def assert_valid_value(value)
if coder.respond_to?(:assert_valid_value)
- coder.assert_valid_value(value)
+ coder.assert_valid_value(value, action: "serialize")
end
end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
new file mode 100644
index 0000000000..cb1949700a
--- /dev/null
+++ b/activerecord/lib/active_record/type/text.rb
@@ -0,0 +1,9 @@
+module ActiveRecord
+ module Type
+ class Text < ActiveModel::Type::String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
index 288fa23efe..9ae0109f9f 100644
--- a/activemodel/lib/active_model/type/unsigned_integer.rb
+++ b/activerecord/lib/active_record/type/unsigned_integer.rb
@@ -1,6 +1,6 @@
-module ActiveModel
+module ActiveRecord
module Type
- class UnsignedInteger < Integer # :nodoc:
+ class UnsignedInteger < ActiveModel::Type::Integer # :nodoc:
private
def max_value
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 6c54792e26..9f7bbe8843 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -12,6 +12,8 @@ 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
attr_reader :table_name
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index 52529a6b42..9f79723125 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -11,6 +11,8 @@ module ActiveRecord
type.serialize(value)
end
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
+ # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :types
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index c013a4518f..9633f226f0 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -68,7 +68,7 @@ module ActiveRecord
alias_method :validate, :valid?
- protected
+ private
def default_validation_context
new_record? ? :create : :update
@@ -78,7 +78,7 @@ module ActiveRecord
raise(RecordInvalid.new(self))
end
- def perform_validations(options = {}) # :nodoc:
+ def perform_validations(options = {})
options[:validate] == false || valid?(options[:context])
end
end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index ca5eda2f84..7cfd55f516 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -57,7 +57,7 @@ module ActiveRecord
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
# proc or string should return or evaluate to a +true+ or +false+ value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
- # See ActiveModel::Validation#validates! for more information.
+ # See ActiveModel::Validations#validates! for more information.
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index bed93bfc26..154cf5f1a4 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -33,13 +33,13 @@ module ActiveRecord
end
end
- protected
+ private
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
# their subclasses, we have to build the hierarchy between self and
# the record's class.
- def find_finder_class_for(record) #:nodoc:
+ def find_finder_class_for(record)
class_hierarchy = [record.class]
while class_hierarchy.first != @klass
@@ -49,47 +49,16 @@ module ActiveRecord
class_hierarchy.detect { |klass| !klass.abstract_class? }
end
- def build_relation(klass, attribute, value) # :nodoc:
- if reflection = klass._reflect_on_association(attribute)
- attribute = reflection.foreign_key
- value = value.attributes[reflection.klass.primary_key] unless value.nil?
- end
-
- if value.nil?
- return klass.unscoped.where!(attribute => value)
- end
-
- # the attribute may be an aliased attribute
- if klass.attribute_alias?(attribute)
- attribute = klass.attribute_alias(attribute)
- end
-
- attribute_name = attribute.to_s
-
- table = klass.arel_table
- column = klass.columns_hash[attribute_name]
- cast_type = klass.type_for_attribute(attribute_name)
-
- comparison = if !options[:case_sensitive]
- # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
- klass.connection.case_insensitive_comparison(table, attribute, column, value)
- else
- klass.connection.case_sensitive_comparison(table, attribute, column, value)
- end
- klass.unscoped.tap do |scope|
- parts = [comparison]
- binds = [Relation::QueryAttribute.new(attribute_name, value, cast_type)]
- scope.where_clause += Relation::WhereClause.new(parts, binds)
- end
+ def build_relation(klass, attribute, value)
+ klass.unscoped.where!({ attribute => value }, options)
end
def scope_relation(record, relation)
Array(options[:scope]).each do |scope_item|
- if reflection = record.class._reflect_on_association(scope_item)
- scope_value = record.send(reflection.foreign_key)
- scope_item = reflection.foreign_key
+ scope_value = if record.class._reflect_on_association(scope_item)
+ record.association(scope_item).reader
else
- scope_value = record._read_attribute(scope_item)
+ record._read_attribute(scope_item)
end
relation = relation.where(scope_item => scope_value)
end
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
index 4263c11ffc..47c0981a49 100644
--- a/activerecord/lib/rails/generators/active_record/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -20,6 +20,14 @@ module ActiveRecord
key_type = options[:primary_key_type]
", id: :#{key_type}" if key_type
end
+
+ def db_migrate_path
+ if defined?(Rails.application) && Rails.application
+ Rails.application.config.paths["db/migrate"].to_ary.first
+ else
+ "db/migrate"
+ end
+ end
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index 12d1f58f67..1f1c47499b 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -10,12 +10,16 @@ module ActiveRecord
def create_migration_file
set_local_assigns!
validate_file_name!
- migration_template @migration_template, "db/migrate/#{file_name}.rb"
+ 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
+
# 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
# variables are set up.
@@ -52,7 +56,6 @@ module ActiveRecord
end.to_sym
end
- private
def attributes_with_index
attributes.select { |a| !a.reference? && a.has_index? }
end
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index f1ddc61688..5cec07d2e3 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -17,7 +17,7 @@ module ActiveRecord
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
- migration_template "../../migration/templates/create_table_migration.rb", "db/migrate/create_#{table_name}.rb"
+ migration_template "../../migration/templates/create_table_migration.rb", File.join(db_migrate_path, "create_#{table_name}.rb")
end
def create_model_file
@@ -33,7 +33,7 @@ module ActiveRecord
hook_for :test_framework
- protected
+ private
def attributes_with_index
attributes.select { |a| !a.reference? && a.has_index? }
@@ -41,7 +41,7 @@ module ActiveRecord
# FIXME: Change this file to a symlink once RubyGems 2.5.0 is required.
def generate_application_record
- if self.behavior == :invoke && !application_record_exist?
+ if behavior == :invoke && !application_record_exist?
template "application_record.rb", application_record_file_name
end
end
diff --git a/activerecord/test/assets/schema_dump_5_1.yml b/activerecord/test/assets/schema_dump_5_1.yml
new file mode 100644
index 0000000000..f37977daf2
--- /dev/null
+++ b/activerecord/test/assets/schema_dump_5_1.yml
@@ -0,0 +1,345 @@
+--- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache
+columns:
+ posts:
+ - &1 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: id
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: INTEGER
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - &2 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: author_id
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - &3 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: title
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: varchar
+ type: :string
+ limit:
+ precision:
+ scale:
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - &4 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: body
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: text
+ type: :text
+ limit:
+ precision:
+ scale:
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - &5 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: type
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: varchar
+ type: :string
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default:
+ default_function:
+ collation:
+ comment:
+ - &6 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: comments_count
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: '0'
+ default_function:
+ collation:
+ comment:
+ - &7 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: taggings_with_delete_all_count
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: '0'
+ default_function:
+ collation:
+ comment:
+ - &8 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: taggings_with_destroy_count
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: '0'
+ default_function:
+ collation:
+ comment:
+ - &9 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: tags_count
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: '0'
+ default_function:
+ collation:
+ comment:
+ - &10 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: tags_with_destroy_count
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: '0'
+ default_function:
+ collation:
+ comment:
+ - &11 !ruby/object:ActiveRecord::ConnectionAdapters::Column
+ name: tags_with_nullify_count
+ table_name: posts
+ sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
+ sql_type: integer
+ type: :integer
+ limit:
+ precision:
+ scale:
+ 'null': true
+ default: '0'
+ default_function:
+ collation:
+ comment:
+columns_hash:
+ posts:
+ id: *1
+ author_id: *2
+ title: *3
+ body: *4
+ type: *5
+ comments_count: *6
+ taggings_with_delete_all_count: *7
+ taggings_with_destroy_count: *8
+ tags_count: *9
+ tags_with_destroy_count: *10
+ tags_with_nullify_count: *11
+primary_keys:
+ posts: id
+data_sources:
+ ar_internal_metadata: true
+ table_with_autoincrement: true
+ accounts: true
+ admin_accounts: true
+ admin_users: true
+ aircraft: true
+ articles: true
+ articles_magazines: true
+ articles_tags: true
+ audit_logs: true
+ authors: true
+ author_addresses: true
+ author_favorites: true
+ auto_id_tests: true
+ binaries: true
+ birds: true
+ books: true
+ booleans: true
+ bulbs: true
+ CamelCase: true
+ cars: true
+ carriers: true
+ categories: true
+ categories_posts: true
+ categorizations: true
+ citations: true
+ clubs: true
+ collections: true
+ colnametests: true
+ columns: true
+ comments: true
+ companies: true
+ content: true
+ content_positions: true
+ vegetables: true
+ computers: true
+ computers_developers: true
+ contracts: true
+ customers: true
+ customer_carriers: true
+ dashboards: true
+ developers: true
+ developers_projects: true
+ dog_lovers: true
+ dogs: true
+ doubloons: true
+ edges: true
+ engines: true
+ entrants: true
+ essays: true
+ events: true
+ eyes: true
+ funny_jokes: true
+ cold_jokes: true
+ friendships: true
+ goofy_string_id: true
+ having: true
+ guids: true
+ guitars: true
+ inept_wizards: true
+ integer_limits: true
+ invoices: true
+ iris: true
+ items: true
+ jobs: true
+ jobs_pool: true
+ keyboards: true
+ legacy_things: true
+ lessons: true
+ lessons_students: true
+ students: true
+ lint_models: true
+ line_items: true
+ lions: true
+ lock_without_defaults: true
+ lock_without_defaults_cust: true
+ magazines: true
+ mateys: true
+ members: true
+ member_details: true
+ member_friends: true
+ memberships: true
+ member_types: true
+ mentors: true
+ minivans: true
+ minimalistics: true
+ mixed_case_monkeys: true
+ mixins: true
+ movies: true
+ notifications: true
+ numeric_data: true
+ orders: true
+ organizations: true
+ owners: true
+ paint_colors: true
+ paint_textures: true
+ parrots: true
+ parrots_pirates: true
+ parrots_treasures: true
+ people: true
+ peoples_treasures: true
+ personal_legacy_things: true
+ pets: true
+ pets_treasures: true
+ pirates: true
+ posts: true
+ serialized_posts: true
+ images: true
+ price_estimates: true
+ products: true
+ product_types: true
+ projects: true
+ randomly_named_table1: true
+ randomly_named_table2: true
+ randomly_named_table3: true
+ ratings: true
+ readers: true
+ references: true
+ shape_expressions: true
+ ships: true
+ ship_parts: true
+ prisoners: true
+ shop_accounts: true
+ speedometers: true
+ sponsors: true
+ string_key_objects: true
+ subscribers: true
+ subscriptions: true
+ tags: true
+ taggings: true
+ tasks: true
+ topics: true
+ toys: true
+ traffic_lights: true
+ treasures: true
+ tuning_pegs: true
+ tyres: true
+ variants: true
+ vertices: true
+ warehouse-things: true
+ circles: true
+ squares: true
+ triangles: true
+ non_poly_ones: true
+ non_poly_twos: true
+ men: true
+ faces: true
+ interests: true
+ zines: true
+ wheels: true
+ countries: true
+ treaties: true
+ countries_treaties: true
+ liquid: true
+ molecules: true
+ electrons: true
+ weirds: true
+ nodes: true
+ trees: true
+ hotels: true
+ departments: true
+ cake_designers: true
+ drink_designers: true
+ chefs: true
+ recipes: true
+ records: true
+ overloaded_types: true
+ users: true
+ test_with_keyword_column_name: true
+ fk_test_has_pk: true
+ fk_test_has_fk: true
+version: 0
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 8bcecf2ed3..601d575c0e 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -30,9 +30,18 @@ module ActiveRecord
assert_nothing_raised { Book.destroy(0) }
end
+ def test_valid_column
+ @connection.native_database_types.each_key do |type|
+ assert @connection.valid_type?(type)
+ end
+ end
+
+ def test_invalid_column
+ assert_not @connection.valid_type?(:foobar)
+ end
+
def test_tables
- tables = nil
- ActiveSupport::Deprecation.silence { tables = @connection.tables }
+ tables = @connection.tables
assert_includes tables, "accounts"
assert_includes tables, "authors"
assert_includes tables, "tasks"
@@ -40,17 +49,11 @@ module ActiveRecord
end
def test_table_exists?
- ActiveSupport::Deprecation.silence do
- assert @connection.table_exists?("accounts")
- assert @connection.table_exists?(:accounts)
- assert_not @connection.table_exists?("nonexistingtable")
- assert_not @connection.table_exists?("'")
- assert_not @connection.table_exists?(nil)
- end
- end
-
- def test_table_exists_checking_both_tables_and_views_is_deprecated
- assert_deprecated { @connection.table_exists?("accounts") }
+ assert @connection.table_exists?("accounts")
+ assert @connection.table_exists?(:accounts)
+ assert_not @connection.table_exists?("nonexistingtable")
+ assert_not @connection.table_exists?("'")
+ assert_not @connection.table_exists?(nil)
end
def test_data_sources
@@ -182,35 +185,15 @@ module ActiveRecord
assert_not_nil error.cause
end
- unless current_adapter?(:SQLite3Adapter)
- def test_foreign_key_violations_are_translated_to_specific_exception
- error = assert_raises(ActiveRecord::InvalidForeignKey) do
- # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
- if @connection.prefetch_primary_key?
- id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
- @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)"
- else
- @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
- end
- end
-
- assert_not_nil error.cause
+ def test_not_null_violations_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::NotNullViolation) do
+ Post.create
end
- def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
- klass_has_fk = Class.new(ActiveRecord::Base) do
- self.table_name = "fk_test_has_fk"
- end
-
- error = assert_raises(ActiveRecord::InvalidForeignKey) do
- has_fk = klass_has_fk.new
- has_fk.fk_id = 1231231231
- has_fk.save(validate: false)
- end
-
- assert_not_nil error.cause
- end
+ assert_not_nil error.cause
+ end
+ unless current_adapter?(:SQLite3Adapter)
def test_value_limit_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::ValueTooLong) do
Event.create(title: "abcdefgh")
@@ -218,22 +201,13 @@ module ActiveRecord
assert_not_nil error.cause
end
- end
- def test_disable_referential_integrity
- assert_nothing_raised do
- @connection.disable_referential_integrity do
- # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
- if @connection.prefetch_primary_key?
- id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
- @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)"
- else
- @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
- end
- # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block
- # and will fail (at least on Oracle)
- @connection.execute "DELETE FROM fk_test_has_fk"
+ def test_numeric_value_out_of_ranges_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::RangeError) do
+ Book.connection.create("INSERT INTO books(author_id) VALUES (9223372036854775808)")
end
+
+ assert_not_nil error.cause
end
end
@@ -242,6 +216,15 @@ module ActiveRecord
assert result.is_a?(ActiveRecord::Result)
end
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_select_all_with_legacy_binds
+ post = Post.create!(title: "foo", body: "bar")
+ expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}")
+ result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new.to_sql}", nil, [[nil, post.id]])
+ assert_equal expected.to_hash, result.to_hash
+ end
+ end
+
def test_select_methods_passing_a_association_relation
author = Author.create!(name: "john")
Post.create!(author: author, title: "foo", body: "bar")
@@ -269,25 +252,68 @@ module ActiveRecord
unless current_adapter?(:PostgreSQLAdapter)
def test_log_invalid_encoding
- error = assert_raise ActiveRecord::StatementInvalid do
+ error = assert_raises RuntimeError do
@connection.send :log, "SELECT 'ы' FROM DUAL" do
raise "ы".force_encoding(Encoding::ASCII_8BIT)
end
end
- assert_not_nil error.cause
+ assert_not_nil error.message
+ end
+ end
+ end
+
+ class AdapterForeignKeyTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
+ klass_has_fk = Class.new(ActiveRecord::Base) do
+ self.table_name = "fk_test_has_fk"
+ end
+
+ error = assert_raises(ActiveRecord::InvalidForeignKey) do
+ has_fk = klass_has_fk.new
+ has_fk.fk_id = 1231231231
+ has_fk.save(validate: false)
end
+
+ assert_not_nil error.cause
end
- if current_adapter?(:Mysql2Adapter, :SQLite3Adapter)
- def test_tables_returning_both_tables_and_views_is_deprecated
- assert_deprecated { @connection.tables }
+ def test_foreign_key_violations_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::InvalidForeignKey) do
+ insert_into_fk_test_has_fk
end
+
+ assert_not_nil error.cause
end
- def test_passing_arguments_to_tables_is_deprecated
- assert_deprecated { @connection.tables(:books) }
+ def test_disable_referential_integrity
+ assert_nothing_raised do
+ @connection.disable_referential_integrity do
+ insert_into_fk_test_has_fk
+ # should delete created record as otherwise disable_referential_integrity will try to enable constraints
+ # after executed block and will fail (at least on Oracle)
+ @connection.execute "DELETE FROM fk_test_has_fk"
+ end
+ end
end
+
+ private
+
+ def insert_into_fk_test_has_fk
+ # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
+ if @connection.prefetch_primary_key?
+ id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
+ @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)"
+ else
+ @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
+ end
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index a70eb5a094..67e1efde27 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -28,12 +28,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], length: 15)
+ assert_equal expected, add_index(:people, ["last_name", "first_name"], length: 15)
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) "
assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15 })
+ assert_equal expected, add_index(:people, ["last_name", "first_name"], length: { last_name: 15 })
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15, first_name: 10 })
+ assert_equal expected, add_index(:people, ["last_name", :first_name], length: { last_name: 15, "first_name" => 10 })
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -89,8 +92,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
assert_equal expected, actual
end
- expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
- actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t|
+ expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
+ actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
t.index :last_name, length: 10, using: :btree, algorithm: :copy
end
assert_equal expected, actual
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 9c109d8a24..a2faf43b0d 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -42,7 +42,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
@connection.update("set @@wait_timeout=1")
sleep 2
assert !@connection.active?
-
+ ensure
# Repair all fixture connections so other tests won't break.
@fixture_connections.each(&:verify!)
end
@@ -63,12 +63,25 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
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
+ end
+
def test_execute_after_disconnect
@connection.disconnect!
- assert_raise(ActiveRecord::StatementInvalid) do
+ error = assert_raise(ActiveRecord::StatementInvalid) do
@connection.execute("SELECT 1")
end
+ assert_kind_of Mysql2::Error, error.cause
end
def test_quote_after_disconnect
@@ -84,6 +97,22 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
assert_equal false, @connection.active?
end
+ def test_wait_timeout_as_string
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge(wait_timeout: "60"))
+ result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout")
+ assert_equal 60, result
+ end
+ end
+
+ def test_wait_timeout_as_url
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge("url" => "mysql2:///?wait_timeout=60"))
+ result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout")
+ assert_equal 60, result
+ end
+ end
+
def test_mysql_connection_collation_is_configured
assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection")
assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection")
@@ -119,7 +148,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
end
- def test_passing_arbitary_flags_to_adapter
+ def test_passing_arbitrary_flags_to_adapter
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS))
assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags]
@@ -186,7 +215,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
"expected release_advisory_lock to return false when there was no lock to release"
end
- protected
+ private
def test_lock_free(lock_name)
@connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
index 135789a57d..c131a5169c 100644
--- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
@@ -6,23 +6,27 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
end
test "microsecond precision for MySQL gte 5.6.4" do
- stub_version "5.6.4"
- assert_microsecond_precision
+ stub_version "5.6.4" do
+ assert_microsecond_precision
+ end
end
test "no microsecond precision for MySQL lt 5.6.4" do
- stub_version "5.6.3"
- assert_no_microsecond_precision
+ stub_version "5.6.3" do
+ assert_no_microsecond_precision
+ end
end
test "microsecond precision for MariaDB gte 5.3.0" do
- stub_version "5.5.5-10.1.8-MariaDB-log"
- assert_microsecond_precision
+ stub_version "5.5.5-10.1.8-MariaDB-log" do
+ assert_microsecond_precision
+ end
end
test "no microsecond precision for MariaDB lt 5.3.0" do
- stub_version "5.2.9-MariaDB"
- assert_no_microsecond_precision
+ stub_version "5.2.9-MariaDB" do
+ assert_no_microsecond_precision
+ end
end
private
@@ -41,5 +45,8 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
def stub_version(full_version_string)
@connection.stubs(:full_version).returns(full_version_string)
@connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ yield
+ ensure
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb
index 5d4db1be91..6954006003 100644
--- a/activerecord/test/cases/adapters/mysql2/json_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/json_test.rb
@@ -93,7 +93,7 @@ if ActiveRecord::Base.connection.supports_json?
def test_null_json
@connection.execute "insert into json_data_type (payload) VALUES(null)"
x = JsonDataType.first
- assert_equal(nil, x.payload)
+ assert_nil(x.payload)
end
def test_select_array_json_value
@@ -111,7 +111,7 @@ if ActiveRecord::Base.connection.supports_json?
def test_select_nil_json_after_update
json = JsonDataType.create(payload: "foo")
x = JsonDataType.where(payload: nil).first
- assert_equal(nil, x)
+ assert_nil(x)
json.update_attributes payload: nil
x = JsonDataType.where(payload: nil).first
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index 69336eb906..565130c38f 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -17,17 +17,6 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
end
- def test_valid_column
- with_example_table do
- column = @conn.columns("ex").find { |col| col.name == "id" }
- assert @conn.valid_type?(column.type)
- end
- end
-
- def test_invalid_column
- assert_not @conn.valid_type?(:foobar)
- end
-
def test_columns_for_distinct_zero_orders
assert_equal "posts.id",
@conn.columns_for_distinct("posts.id", [])
@@ -65,6 +54,19 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
@conn.columns_for_distinct("posts.id", [order])
end
+ def test_errors_for_bigint_fks_on_integer_pk_table
+ # table old_cars has primary key of integer
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.add_reference :engines, :old_car
+ @conn.add_foreign_key :engines, :old_cars
+ end
+
+ assert_match "Column `old_car_id` on table `engines` has a type of `bigint(20)`", error.message
+ assert_not_nil error.cause
+ @conn.exec_query("ALTER TABLE engines DROP COLUMN old_car_id")
+ end
+
private
def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block)
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index b400f4d2f9..2c778b1150 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -143,7 +143,7 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase
end
# custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns
- def create_tables_directly (tables, connection = @connection)
+ def create_tables_directly(tables, connection = @connection)
tables.each do |table_name, column_properties|
connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )")
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index fa54aac6b3..605baa9905 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -16,7 +16,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
table_name = ActiveRecord::SchemaMigration.table_name
connection.drop_table table_name, if_exists: true
- connection.initialize_schema_migrations_table
+ ActiveRecord::SchemaMigration.create_table
assert connection.column_exists?(table_name, :version, :string, collation: "utf8_general_ci")
end
@@ -27,7 +27,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
table_name = ActiveRecord::InternalMetadata.table_name
connection.drop_table table_name, if_exists: true
- connection.initialize_internal_metadata_table
+ ActiveRecord::InternalMetadata.create_table
assert connection.column_exists?(table_name, :key, :string, collation: "utf8_general_ci")
end
diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
index bee42d48f1..d6e7f29a5c 100644
--- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
@@ -8,7 +8,7 @@ class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase
assert_equal "blob", type_to_sql(:binary)
end
- def type_to_sql(*args)
- ActiveRecord::Base.connection.type_to_sql(*args)
+ def type_to_sql(type, limit = nil)
+ ActiveRecord::Base.connection.type_to_sql(type, limit: limit)
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
index edd5353ee3..16101e38cb 100644
--- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
@@ -10,6 +10,8 @@ module ActiveRecord
end
setup do
+ @abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+
@connection = ActiveRecord::Base.connection
@connection.clear_cache!
@@ -25,30 +27,34 @@ module ActiveRecord
teardown do
@connection.drop_table "samples", if_exists: true
+
+ Thread.abort_on_exception = @abort
end
test "raises Deadlocked when a deadlock is encountered" do
assert_raises(ActiveRecord::Deadlocked) do
+ barrier = Concurrent::CyclicBarrier.new(2)
+
s1 = Sample.create value: 1
s2 = Sample.create value: 2
thread = Thread.new do
Sample.transaction do
s1.lock!
- sleep 1
+ barrier.wait
s2.update_attributes value: 1
end
end
- sleep 0.5
-
- Sample.transaction do
- s2.lock!
- sleep 1
- s1.update_attributes value: 2
+ begin
+ Sample.transaction do
+ s2.lock!
+ barrier.wait
+ s1.update_attributes value: 2
+ end
+ ensure
+ thread.join
end
-
- thread.join
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 452f8d5ae8..a0823be143 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -15,6 +15,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
t.bigint :unsigned_bigint, unsigned: true
t.float :unsigned_float, unsigned: true
t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
+ t.column :unsigned_zerofill, "int unsigned zerofill"
end
end
@@ -34,10 +35,10 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
assert_raise(ActiveModel::RangeError) do
UnsignedType.create(unsigned_bigint: -10)
end
- assert_raise(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::RangeError) do
UnsignedType.create(unsigned_float: -10.0)
end
- assert_raise(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::RangeError) do
UnsignedType.create(unsigned_decimal: -10.0)
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
new file mode 100644
index 0000000000..442a4fb7b5
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
@@ -0,0 +1,59 @@
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+if ActiveRecord::Base.connection.supports_virtual_columns?
+ class Mysql2VirtualColumnTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ class VirtualColumn < ActiveRecord::Base
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :virtual_columns, force: true do |t|
+ t.string :name
+ t.virtual :upper_name, type: :string, as: "UPPER(`name`)"
+ t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true
+ end
+ VirtualColumn.create(name: "Rails")
+ end
+
+ def teardown
+ @connection.drop_table :virtual_columns, if_exists: true
+ VirtualColumn.reset_column_information
+ end
+
+ def test_virtual_column
+ column = VirtualColumn.columns_hash["upper_name"]
+ assert_predicate column, :virtual?
+ assert_match %r{\bVIRTUAL\b}, column.extra
+ assert_equal "RAILS", VirtualColumn.take.upper_name
+ end
+
+ def test_stored_column
+ column = VirtualColumn.columns_hash["name_length"]
+ assert_predicate column, :virtual?
+ assert_match %r{\b(?:STORED|PERSISTENT)\b}, column.extra
+ assert_equal 5, VirtualColumn.take.name_length
+ end
+
+ def test_change_table
+ @connection.change_table :virtual_columns do |t|
+ t.virtual :lower_name, type: :string, as: "LOWER(name)"
+ end
+ VirtualColumn.reset_column_information
+ column = VirtualColumn.columns_hash["lower_name"]
+ assert_predicate column, :virtual?
+ assert_match %r{\bVIRTUAL\b}, column.extra
+ assert_equal "rails", VirtualColumn.take.lower_name
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("virtual_columns")
+ assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output)
+ assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output)
+ end
+ 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 d3c65f3d94..b787de8453 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -39,6 +39,10 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name"))
assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently)
+ expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC))
+ assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc })
+ assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc })
+
%w(gin gist hash btree).each do |type|
expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
assert_equal expected, add_index(:people, :last_name, using: type)
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 680dad9706..c78c6178ff 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -16,10 +16,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
@connection.transaction do
@connection.create_table("pg_arrays") do |t|
- t.string "tags", array: true
+ t.string "tags", array: true, limit: 255
t.integer "ratings", array: true
t.datetime :datetimes, array: true
t.hstore :hstores, array: true
+ t.decimal :decimals, array: true, default: [], precision: 10, scale: 2
+ t.timestamp :timestamps, array: true, default: [], precision: 6
end
end
PgArray.reset_column_information
@@ -34,7 +36,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_column
assert_equal :string, @column.type
- assert_equal "character varying", @column.sql_type
+ assert_equal "character varying(255)", @column.sql_type
assert @column.array?
assert_not @type.binary?
@@ -110,8 +112,9 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_schema_dump_with_shorthand
output = dump_table_schema "pg_arrays"
- assert_match %r[t\.string\s+"tags",\s+array: true], output
+ assert_match %r[t\.string\s+"tags",\s+limit: 255,\s+array: true], output
assert_match %r[t\.integer\s+"ratings",\s+array: true], output
+ assert_match %r[t\.decimal\s+"decimals",\s+precision: 10,\s+scale: 2,\s+default: \[\],\s+array: true], output
end
def test_select_with_strings
@@ -211,7 +214,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x = PgArray.create!(tags: tags)
x.reload
- assert_equal x.tags_before_type_cast, PgArray.type_for_attribute("tags").serialize(tags)
+ refute x.changed?
end
def test_quoting_non_standard_delimiters
@@ -219,9 +222,10 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",")
semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";")
+ conn = PgArray.connection
- assert_equal %({"hello,",world;}), comma_delim.serialize(strings)
- assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings)
+ assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings))
+ assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings))
end
def test_mutate_array
@@ -317,6 +321,15 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings]))
end
+ def test_precision_is_respected_on_timestamp_columns
+ time = Time.now.change(usec: 123)
+ record = PgArray.create!(timestamps: [time])
+
+ assert_equal 123, record.timestamps.first.usec
+ record.reload
+ assert_equal 123, record.timestamps.first.usec
+ end
+
private
def assert_cycle(field, array)
# test creation
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index dc0df8715a..99175e8091 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -32,9 +32,9 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
end
def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
- assert_equal "bytea", @connection.type_to_sql(:binary, 100_000)
+ assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000)
assert_raise ActiveRecord::ActiveRecordError do
- @connection.type_to_sql :binary, 4294967295
+ @connection.type_to_sql(:binary, limit: 4294967295)
end
end
@@ -52,7 +52,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
end
def test_type_case_nil
- assert_equal(nil, @type.deserialize(nil))
+ assert_nil(@type.deserialize(nil))
end
def test_read_value
@@ -66,7 +66,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_read_nil_value
@connection.execute "insert into bytea_data_type (payload) VALUES (null)"
record = ByteaDataType.first
- assert_equal(nil, record.payload)
+ assert_nil(record.payload)
record.delete
end
@@ -89,6 +89,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
Thread.new do
other_conn = ActiveRecord::Base.connection
other_conn.execute("SET standard_conforming_strings = off")
+ other_conn.execute("SET escape_string_warning = off")
end.join
test_via_to_sql
@@ -106,8 +107,8 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_write_nil
record = ByteaDataType.create(payload: nil)
assert_not record.new_record?
- assert_equal(nil, record.payload)
- assert_equal(nil, ByteaDataType.where(id: record.id).first.payload)
+ assert_nil(record.payload)
+ assert_nil(ByteaDataType.where(id: record.id).first.payload)
end
class Serializer
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 48c82cb7b9..c52d9e37cc 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -90,22 +90,22 @@ module ActiveRecord
end
def test_tables_logs_name
- ActiveSupport::Deprecation.silence { @connection.tables("hello") }
+ @connection.tables
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_indexes_logs_name
- @connection.indexes("items", "hello")
+ assert_deprecated { @connection.indexes("items", "hello") }
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_table_exists_logs_name
- ActiveSupport::Deprecation.silence { @connection.table_exists?("items") }
+ @connection.table_exists?("items")
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_table_alias_length_logs_name
- @connection.instance_variable_set("@table_alias_length", nil)
+ @connection.instance_variable_set("@max_identifier_length", nil)
@connection.table_alias_length
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
@@ -156,7 +156,7 @@ module ActiveRecord
secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})")
ActiveRecord::Base.connection_pool.checkin(secondary_connection)
elsif ARTest.config["with_manual_interventions"]
- puts "Kill the connection now (e.g. by restarting the PostgreSQL " +
+ puts "Kill the connection now (e.g. by restarting the PostgreSQL " \
'server with the "-m fast" option) and then press enter.'
$stdin.gets
else
@@ -175,9 +175,9 @@ module ActiveRecord
new_connection_pid = @connection.query("select pg_backend_pid()")
assert_not_equal original_connection_pid, new_connection_pid,
- "umm -- looks like you didn't break the connection, because we're still " +
+ "umm -- looks like you didn't break the connection, because we're still " \
"successfully querying with the same connection pid."
-
+ ensure
# Repair all fixture connections so other tests won't break.
@fixture_connections.each(&:verify!)
end
@@ -245,7 +245,7 @@ module ActiveRecord
end
end
- protected
+ private
def with_warning_suppression
log_level = @connection.client_min_messages
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index 0ac8b7339b..0725fde5ae 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -28,12 +28,12 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
end
def test_data_type_of_time_types
- assert_equal :string, @first_time.column_for_attribute(:time_interval).type
- assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type
+ assert_equal :interval, @first_time.column_for_attribute(:time_interval).type
+ assert_equal :interval, @first_time.column_for_attribute(:scaled_time_interval).type
end
def test_data_type_of_oid_types
- assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
+ assert_equal :oid, @first_oid.column_for_attribute(:obj_id).type
end
def test_time_values
@@ -61,9 +61,9 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
end
def test_text_columns_are_limitless_the_upper_limit_is_one_GB
- assert_equal "text", @connection.type_to_sql(:text, 100_000)
+ assert_equal "text", @connection.type_to_sql(:text, limit: 100_000)
assert_raise ActiveRecord::ActiveRecordError do
- @connection.type_to_sql :text, 4294967295
+ @connection.type_to_sql(:text, limit: 4294967295)
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index a65d4d1ad9..c1f3a4ae2c 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -96,7 +96,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
assert_nothing_raised { PostgresqlPoint.new(x: "") }
p = PostgresqlPoint.new(x: "")
- assert_equal nil, p.x
+ assert_nil p.x
end
def test_array_of_points_round_trip
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 1f35300739..f9cce10fb8 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -171,6 +171,25 @@ if ActiveRecord::Base.connection.supports_extensions?
assert_not hstore.changed?
end
+ def test_dirty_from_user_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
+
+ hstore.settings = { "key" => "value", "alongkey" => "anything" }
+ assert_equal settings, hstore.settings
+ refute hstore.changed?
+ end
+
+ def test_hstore_dirty_from_database_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
+ hstore.reload
+
+ assert_equal settings, hstore.settings
+ hstore.settings = settings
+ refute hstore.changed?
+ end
+
def test_gen1
assert_equal('" "=>""', @type.serialize(" " => ""))
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index 19b00258b6..b9e177e6ec 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -30,7 +30,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
record = PostgresqlInfinity.new(float: "-Infinity")
assert_equal(-Float::INFINITY, record.float)
record = PostgresqlInfinity.new(float: "NaN")
- assert_send [record.float, :nan?]
+ assert record.float.nan?, "Expected #{record.float} to be NaN"
end
test "update_all with infinity on a float column" do
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index 991dedfdf1..d4e627001c 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -16,6 +16,7 @@ module PostgresqlJSONSharedTestCases
@connection.create_table("json_data_type") do |t|
t.public_send column_type, "payload", default: {} # t.json 'payload', default: {}
t.public_send column_type, "settings" # t.json 'settings'
+ t.public_send column_type, "objects", array: true # t.json 'objects', array: true
end
rescue ActiveRecord::StatementInvalid
skip "do not test on PostgreSQL without #{column_type} type."
@@ -75,6 +76,15 @@ module PostgresqlJSONSharedTestCases
assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload)
end
+ def test_deserialize_with_array
+ x = JsonDataType.new(objects: ["foo" => "bar"])
+ assert_equal ["foo" => "bar"], x.objects
+ x.save!
+ assert_equal ["foo" => "bar"], x.objects
+ x.reload
+ assert_equal ["foo" => "bar"], x.objects
+ end
+
def test_type_cast_json
type = JsonDataType.type_for_attribute("payload")
@@ -110,7 +120,7 @@ module PostgresqlJSONSharedTestCases
def test_null_json
@connection.execute "insert into json_data_type (payload) VALUES(null)"
x = JsonDataType.first
- assert_equal(nil, x.payload)
+ assert_nil(x.payload)
end
def test_select_nil_json_after_create
@@ -122,7 +132,7 @@ module PostgresqlJSONSharedTestCases
def test_select_nil_json_after_update
json = JsonDataType.create(payload: "foo")
x = JsonDataType.where(payload: nil).first
- assert_equal(nil, x)
+ assert_nil(x)
json.update_attributes payload: nil
x = JsonDataType.where(payload: nil).first
diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
index 834354dcc9..bfb2b7c27a 100644
--- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
@@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase
assert_equal 123456.789, first.double
assert_equal(-::Float::INFINITY, second.single)
assert_equal ::Float::INFINITY, second.double
- assert_send [third.double, :nan?]
+ assert third.double.nan?, "Expected #{third.double} to be NaN"
end
def test_update
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index e6af93a53e..003e6e62e7 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -21,17 +21,6 @@ module ActiveRecord
end
end
- def test_valid_column
- with_example_table do
- column = @connection.columns("ex").find { |col| col.name == "id" }
- assert @connection.valid_type?(column.type)
- end
- end
-
- def test_invalid_column
- assert_not @connection.valid_type?(:foobar)
- end
-
def test_primary_key
with_example_table do
assert_equal "id", @connection.primary_key("ex")
@@ -54,12 +43,6 @@ module ActiveRecord
end
end
- def test_primary_key_raises_error_if_table_not_found
- assert_raises(ActiveRecord::StatementInvalid) do
- @connection.primary_key("unobtainium")
- end
- end
-
def test_exec_insert_with_returning_disabled
connection = connection_without_insert_returning
result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id", "postgresql_partitioned_table_parent_id_seq")
@@ -263,9 +246,12 @@ module ActiveRecord
def test_index_with_opclass
with_example_table do
- @connection.add_index "ex", "data varchar_pattern_ops", name: "with_opclass"
- index = @connection.indexes("ex").find { |idx| idx.name == "with_opclass" }
+ @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.remove_index "ex", "data varchar_pattern_ops"
+ assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index a9e81ab3d8..a1e966b915 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -1,5 +1,4 @@
require "cases/helper"
-require "ipaddr"
module ActiveRecord
module ConnectionAdapters
@@ -36,7 +35,7 @@ module ActiveRecord
def test_quote_bit_string
value = "'); SELECT * FROM users; /*\n01\n*/--"
type = OID::Bit.new
- assert_equal nil, @conn.quote(type.serialize(value))
+ assert_nil @conn.quote(type.serialize(value))
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
index 0ff04bfa27..c5c540cebc 100644
--- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
@@ -1,111 +1,150 @@
require "cases/helper"
require "support/connection_helper"
-class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
- self.use_transactional_tests = false
+if ActiveRecord::Base.connection.respond_to?(:supports_alter_constraint?) &&
+ ActiveRecord::Base.connection.supports_alter_constraint?
+ class PostgreSQLReferentialIntegrityWithAlterConstraintTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
- include ConnectionHelper
+ include ConnectionHelper
- IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql|
- sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/)
- end
+ IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql|
+ sql.match(/SET CONSTRAINTS ALL DEFERRED/)
+ end
- module MissingSuperuserPrivileges
- def execute(sql)
- if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
- super "BROKEN;" rescue nil # put transaction in broken state
- raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege"
- else
- super
+ module ProgrammerMistake
+ def execute(sql)
+ if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
+ raise ArgumentError, "something is not right."
+ else
+ super
+ end
end
end
- end
- module ProgrammerMistake
- def execute(sql)
- if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
- raise ArgumentError, "something is not right."
- else
- super
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ reset_connection
+ end
+
+ def test_errors_bubble_up
+ @connection.extend ProgrammerMistake
+
+ assert_raises ArgumentError do
+ @connection.disable_referential_integrity {}
end
end
end
+else
+ class PostgreSQLReferentialIntegrityWithDisableTriggerTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
- def setup
- @connection = ActiveRecord::Base.connection
- end
+ include ConnectionHelper
- def teardown
- reset_connection
- if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges)
- raise "MissingSuperuserPrivileges patch was not removed"
+ IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql|
+ sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/)
end
- end
- def test_should_reraise_invalid_foreign_key_exception_and_show_warning
- @connection.extend MissingSuperuserPrivileges
+ module MissingSuperuserPrivileges
+ def execute(sql)
+ if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
+ super "BROKEN;" rescue nil # put transaction in broken state
+ raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege"
+ else
+ super
+ end
+ end
+ end
- warning = capture(:stderr) do
- e = assert_raises(ActiveRecord::InvalidForeignKey) do
- @connection.disable_referential_integrity do
- raise ActiveRecord::InvalidForeignKey, "Should be re-raised"
+ module ProgrammerMistake
+ def execute(sql)
+ if IS_REFERENTIAL_INTEGRITY_SQL.call(sql)
+ raise ArgumentError, "something is not right."
+ else
+ super
end
end
- assert_equal "Should be re-raised", e.message
end
- assert_match (/WARNING: Rails was not able to disable referential integrity/), warning
- assert_match (/cause: PG::InsufficientPrivilege/), warning
- end
- def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised
- @connection.extend MissingSuperuserPrivileges
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
- warning = capture(:stderr) do
- e = assert_raises(ActiveRecord::StatementInvalid) do
- @connection.disable_referential_integrity do
- raise ActiveRecord::StatementInvalid, "Should be re-raised"
+ def teardown
+ reset_connection
+ if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges)
+ raise "MissingSuperuserPrivileges patch was not removed"
+ end
+ end
+
+ def test_should_reraise_invalid_foreign_key_exception_and_show_warning
+ @connection.extend MissingSuperuserPrivileges
+
+ warning = capture(:stderr) do
+ e = assert_raises(ActiveRecord::InvalidForeignKey) do
+ @connection.disable_referential_integrity do
+ raise ActiveRecord::InvalidForeignKey, "Should be re-raised"
+ end
end
+ assert_equal "Should be re-raised", e.message
end
- assert_equal "Should be re-raised", e.message
+ assert_match (/WARNING: Rails was not able to disable referential integrity/), warning
+ assert_match (/cause: PG::InsufficientPrivilege/), warning
end
- assert warning.blank?, "expected no warnings but got:\n#{warning}"
- end
- def test_does_not_break_transactions
- @connection.extend MissingSuperuserPrivileges
+ def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised
+ @connection.extend MissingSuperuserPrivileges
- @connection.transaction do
- @connection.disable_referential_integrity do
- assert_transaction_is_not_broken
+ warning = capture(:stderr) do
+ e = assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.disable_referential_integrity do
+ raise ActiveRecord::StatementInvalid, "Should be re-raised"
+ end
+ end
+ assert_equal "Should be re-raised", e.message
end
- assert_transaction_is_not_broken
+ assert warning.blank?, "expected no warnings but got:\n#{warning}"
end
- end
- def test_does_not_break_nested_transactions
- @connection.extend MissingSuperuserPrivileges
+ def test_does_not_break_transactions
+ @connection.extend MissingSuperuserPrivileges
- @connection.transaction do
- @connection.transaction(requires_new: true) do
+ @connection.transaction do
@connection.disable_referential_integrity do
assert_transaction_is_not_broken
end
+ assert_transaction_is_not_broken
end
- assert_transaction_is_not_broken
end
- end
- def test_only_catch_active_record_errors_others_bubble_up
- @connection.extend ProgrammerMistake
+ def test_does_not_break_nested_transactions
+ @connection.extend MissingSuperuserPrivileges
- assert_raises ArgumentError do
- @connection.disable_referential_integrity {}
+ @connection.transaction do
+ @connection.transaction(requires_new: true) do
+ @connection.disable_referential_integrity do
+ assert_transaction_is_not_broken
+ end
+ end
+ assert_transaction_is_not_broken
+ end
end
- end
- private
+ def test_only_catch_active_record_errors_others_bubble_up
+ @connection.extend ProgrammerMistake
- def assert_transaction_is_not_broken
- assert_equal 1, @connection.select_value("SELECT 1")
+ assert_raises ArgumentError do
+ @connection.disable_referential_integrity {}
+ end
end
+
+ private
+
+ def assert_transaction_is_not_broken
+ assert_equal 1, @connection.select_value("SELECT 1")
+ end
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 237e9ff6a5..7b065ff320 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -301,13 +301,13 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_index_name_exists
with_schema_search_path(SCHEMA_NAME) do
- assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true)
- assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true)
- assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true)
- assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true)
- assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true)
- assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true)
- assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index", true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME)
+ assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index")
end
end
@@ -366,14 +366,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
end
- def test_primary_key_raises_error_if_table_not_found_on_schema_search_path
- with_schema_search_path(SCHEMA2_NAME) do
- assert_raises(ActiveRecord::StatementInvalid) do
- @connection.primary_key(PK_TABLE_NAME)
- end
- end
- end
-
def test_pk_and_sequence_for_with_schema_specified
pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
[
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index eb9978a898..146b619a4b 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -3,13 +3,13 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
- class InactivePGconn
+ class InactivePgConnection
def query(*args)
- raise PGError
+ raise PG::Error
end
def status
- PGconn::CONNECTION_BAD
+ PG::CONNECTION_BAD
end
end
@@ -31,7 +31,7 @@ module ActiveRecord
end
def test_dealloc_does_not_raise_on_inactive_connection
- cache = StatementPool.new InactivePGconn.new, 10
+ cache = StatementPool.new InactivePgConnection.new, 10
cache["foo"] = "bar"
assert_nothing_raised { cache.clear }
end
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
index c450524de8..9b42d0383d 100644
--- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -11,6 +11,8 @@ module ActiveRecord
end
setup do
+ @abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+
@connection = ActiveRecord::Base.connection
@connection.transaction do
@@ -25,6 +27,8 @@ module ActiveRecord
teardown do
@connection.drop_table "samples", if_exists: true
+
+ Thread.abort_on_exception = @abort
end
test "raises SerializationFailure when a serialization failure occurs" do
@@ -85,7 +89,7 @@ module ActiveRecord
end
end
- protected
+ private
def with_warning_suppression
log_level = ActiveRecord::Base.connection.client_min_messages
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
index bd45a9daa0..784d77a8d1 100644
--- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -19,7 +19,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
big_array = [123456789123456789]
assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) }
- assert_equal "{123456789123456789}", bigint_array.serialize(big_array)
+ assert_equal "{123456789123456789}", @connection.type_cast(bigint_array.serialize(big_array))
end
test "range types correctly respect registration of subtypes" do
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 7f6ea3887d..9f9e3bda2f 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -56,7 +56,7 @@ class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase
test "can be used as hash key" do
hash = { Name.new("schema", "article_seq") => "success" }
assert_equal "success", hash[Name.new("schema", "article_seq")]
- assert_equal nil, hash[Name.new("schema", "articles")]
- assert_equal nil, hash[Name.new("public", "article_seq")]
+ assert_nil hash[Name.new("schema", "articles")]
+ assert_nil hash[Name.new("public", "article_seq")]
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 9a59691737..6aa6a79705 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -9,6 +9,10 @@ module PostgresqlUUIDHelper
def drop_table(name)
connection.drop_table name, if_exists: true
end
+
+ def uuid_function
+ connection.supports_pgcrypto_uuid? ? "gen_random_uuid()" : "uuid_generate_v4()"
+ end
end
class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
@@ -21,6 +25,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
setup do
enable_extension!("uuid-ossp", connection)
+ enable_extension!("pgcrypto", connection) if connection.supports_pgcrypto_uuid?
connection.create_table "uuid_data_type" do |t|
t.uuid "guid"
@@ -31,14 +36,22 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
drop_table "uuid_data_type"
end
+ if ActiveRecord::Base.connection.supports_pgcrypto_uuid?
+ def test_uuid_column_default
+ connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()"
+ UUIDType.reset_column_information
+ column = UUIDType.columns_hash["thingy"]
+ assert_equal "gen_random_uuid()", column.default_function
+ end
+ end
+
def test_change_column_default
- @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()"
+ connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()"
UUIDType.reset_column_information
column = UUIDType.columns_hash["thingy"]
assert_equal "uuid_generate_v1()", column.default_function
- @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()"
-
+ connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()"
UUIDType.reset_column_information
column = UUIDType.columns_hash["thingy"]
assert_equal "uuid_generate_v4()", column.default_function
@@ -58,12 +71,12 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
def test_treat_blank_uuid_as_nil
UUIDType.create! guid: ""
- assert_equal(nil, UUIDType.last.guid)
+ assert_nil(UUIDType.last.guid)
end
def test_treat_invalid_uuid_as_nil
uuid = UUIDType.create! guid: "foobar"
- assert_equal(nil, uuid.guid)
+ assert_nil(uuid.guid)
end
def test_invalid_uuid_dont_modify_before_type_cast
@@ -155,7 +168,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
# to test dumping tables which columns have defaults with custom functions
connection.execute <<-SQL
CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
- AS $$ SELECT * FROM uuid_generate_v4() $$
+ AS $$ SELECT * FROM #{uuid_function} $$
LANGUAGE SQL VOLATILE;
SQL
@@ -164,11 +177,16 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
t.string "name"
t.uuid "other_uuid_2", default: "my_uuid_generator()"
end
+
+ connection.create_table("pg_uuids_3", id: :uuid) do |t|
+ t.string "name"
+ end
end
teardown do
drop_table "pg_uuids"
drop_table "pg_uuids_2"
+ drop_table "pg_uuids_3"
connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();"
end
@@ -192,7 +210,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
def test_pk_and_sequence_for_uuid_primary_key
pk, seq = connection.pk_and_sequence_for("pg_uuids")
assert_equal "id", pk
- assert_equal nil, seq
+ assert_nil seq
end
def test_schema_dumper_for_uuid_primary_key
@@ -206,6 +224,34 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
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_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
+
+ 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
end
end
@@ -237,6 +283,25 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
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
end
end
@@ -282,7 +347,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase
assert_raise ActiveRecord::RecordNotFound do
UuidPost.find(123456)
end
-
end
def test_find_by_with_uuid
diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
index 8342b05870..e1cfd703e8 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -41,8 +41,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase
test_copy_table("comments", "comments_with_index") do
@connection.add_index("comments_with_index", ["post_id", "type"])
test_copy_table("comments_with_index", "comments_with_index2") do
- assert_equal table_indexes_without_name("comments_with_index"),
- table_indexes_without_name("comments_with_index2")
+ assert_nil table_indexes_without_name("comments_with_index")
+ assert_nil table_indexes_without_name("comments_with_index2")
end
end
end
@@ -59,7 +59,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase
copied_id = @connection.columns("goofy_string_id2").detect { |col| col.name == "id" }
assert_equal original_id.type, copied_id.type
assert_equal original_id.sql_type, copied_id.sql_type
- assert_equal original_id.limit, copied_id.limit
+ assert_nil original_id.limit
+ assert_nil copied_id.limit
end
end
@@ -75,7 +76,7 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase
test_copy_table "binaries", "binaries2"
end
-protected
+private
def copy_table(from, to, options = {})
@connection.copy_table(from, to, { temporary: true }.merge(options))
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 80a37e83ff..aefbb309e6 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -1,6 +1,5 @@
require "cases/helper"
require "bigdecimal"
-require "yaml"
require "securerandom"
class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
@@ -15,31 +14,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
assert_equal expected, @conn.type_cast(binary)
end
- def test_type_cast_symbol
- assert_equal "foo", @conn.type_cast(:foo)
- end
-
- def test_type_cast_date
- date = Date.today
- expected = @conn.quoted_date(date)
- assert_equal expected, @conn.type_cast(date)
- end
-
- def test_type_cast_time
- time = Time.now
- expected = @conn.quoted_date(time)
- assert_equal expected, @conn.type_cast(time)
- end
-
- def test_type_cast_numeric
- assert_equal 10, @conn.type_cast(10)
- assert_equal 2.2, @conn.type_cast(2.2)
- end
-
- def test_type_cast_nil
- assert_equal nil, @conn.type_cast(nil)
- end
-
def test_type_cast_true
assert_equal "t", @conn.type_cast(true)
end
@@ -53,31 +27,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
assert_equal bd.to_f, @conn.type_cast(bd)
end
- def test_type_cast_unknown_should_raise_error
- 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
-
def test_quoting_binary_strings
value = "hello".encode("ascii-8bit")
type = ActiveRecord::Type::String.new
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 0526191952..2179d1294c 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -49,22 +49,6 @@ module ActiveRecord
end
end
- def test_valid_column
- with_example_table do
- column = @conn.columns("ex").find { |col| col.name == "id" }
- assert @conn.valid_type?(column.type)
- end
- end
-
- # sqlite3 databases should be able to support any type and not just the
- # ones mentioned in the native_database_types.
- #
- # Therefore test_invalid column should always return true even if the
- # type is not valid.
- def test_invalid_column
- assert @conn.valid_type?(:foobar)
- end
-
def test_column_types
owner = Owner.create!(name: "hello".encode("ascii-8bit"))
owner.reload
@@ -267,29 +251,26 @@ module ActiveRecord
def test_tables
with_example_table do
- ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables }
+ assert_equal %w{ ex }, @conn.tables
with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer", "people" do
- ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort }
+ assert_equal %w{ ex people }.sort, @conn.tables.sort
end
end
end
def test_tables_logs_name
sql = <<-SQL
- SELECT name FROM sqlite_master
- WHERE type IN ('table','view') AND name <> 'sqlite_sequence'
+ SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table')
SQL
assert_logged [[sql.squish, "SCHEMA", []]] do
- ActiveSupport::Deprecation.silence do
- @conn.tables("hello")
- end
+ @conn.tables
end
end
def test_indexes_logs_name
with_example_table do
assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
- @conn.indexes("ex", "hello")
+ assert_deprecated { @conn.indexes("ex", "hello") }
end
end
end
@@ -297,13 +278,10 @@ module ActiveRecord
def test_table_exists_logs_name
with_example_table do
sql = <<-SQL
- SELECT name FROM sqlite_master
- WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex'
+ SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table')
SQL
assert_logged [[sql.squish, "SCHEMA", []]] do
- ActiveSupport::Deprecation.silence do
- assert @conn.table_exists?("ex")
- end
+ assert @conn.table_exists?("ex")
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index aebcce3691..37ff973397 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -3,7 +3,6 @@ require "cases/helper"
class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase
if Process.respond_to?(:fork)
def test_cache_is_per_pid
-
cache = ActiveRecord::ConnectionAdapters::SQLite3Adapter::StatementPool.new(10)
cache["foo"] = "bar"
assert_equal "bar", cache["foo"]
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 397ac599b9..5b608d8e83 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -1,146 +1,143 @@
require "cases/helper"
-if ActiveRecord::Base.connection.supports_migrations?
+class ActiveRecordSchemaTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ setup do
+ @original_verbose = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ @connection = ActiveRecord::Base.connection
+ ActiveRecord::SchemaMigration.drop_table
+ end
- class ActiveRecordSchemaTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
+ teardown do
+ @connection.drop_table :fruits rescue nil
+ @connection.drop_table :nep_fruits rescue nil
+ @connection.drop_table :nep_schema_migrations rescue nil
+ @connection.drop_table :has_timestamps rescue nil
+ @connection.drop_table :multiple_indexes rescue nil
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = @original_verbose
+ end
- setup do
- @original_verbose = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
- @connection = ActiveRecord::Base.connection
- ActiveRecord::SchemaMigration.drop_table
- end
+ def test_has_primary_key
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ assert_equal "version", ActiveRecord::SchemaMigration.primary_key
- teardown do
- @connection.drop_table :fruits rescue nil
- @connection.drop_table :nep_fruits rescue nil
- @connection.drop_table :nep_schema_migrations rescue nil
- @connection.drop_table :has_timestamps rescue nil
- @connection.drop_table :multiple_indexes rescue nil
- ActiveRecord::SchemaMigration.delete_all rescue nil
- ActiveRecord::Migration.verbose = @original_verbose
+ ActiveRecord::SchemaMigration.create_table
+ assert_difference "ActiveRecord::SchemaMigration.count", 1 do
+ ActiveRecord::SchemaMigration.create version: 12
end
+ ensure
+ ActiveRecord::SchemaMigration.drop_table
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
+ end
- def test_has_primary_key
- old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
- ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
- assert_equal "version", ActiveRecord::SchemaMigration.primary_key
-
- ActiveRecord::SchemaMigration.create_table
- assert_difference "ActiveRecord::SchemaMigration.count", 1 do
- ActiveRecord::SchemaMigration.create version: 12
+ def test_schema_define
+ ActiveRecord::Schema.define(version: 7) do
+ create_table :fruits do |t|
+ t.column :color, :string
+ t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
+ t.column :texture, :string
+ t.column :flavor, :string
end
- ensure
- ActiveRecord::SchemaMigration.drop_table
- ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
- def test_schema_define
- ActiveRecord::Schema.define(version: 7) do
- create_table :fruits do |t|
- t.column :color, :string
- t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
- t.column :texture, :string
- t.column :flavor, :string
- end
- end
-
- 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
- end
+ 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
+ end
- def test_schema_define_w_table_name_prefix
- table_name = ActiveRecord::SchemaMigration.table_name
- old_table_name_prefix = ActiveRecord::Base.table_name_prefix
- ActiveRecord::Base.table_name_prefix = "nep_"
- ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
- ActiveRecord::Schema.define(version: 7) do
- create_table :fruits do |t|
- t.column :color, :string
- t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
- t.column :texture, :string
- t.column :flavor, :string
- end
+ def test_schema_define_w_table_name_prefix
+ table_name = ActiveRecord::SchemaMigration.table_name
+ old_table_name_prefix = ActiveRecord::Base.table_name_prefix
+ ActiveRecord::Base.table_name_prefix = "nep_"
+ ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
+ ActiveRecord::Schema.define(version: 7) do
+ create_table :fruits do |t|
+ t.column :color, :string
+ t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
+ t.column :texture, :string
+ t.column :flavor, :string
end
- assert_equal 7, ActiveRecord::Migrator::current_version
- ensure
- ActiveRecord::Base.table_name_prefix = old_table_name_prefix
- ActiveRecord::SchemaMigration.table_name = table_name
end
+ assert_equal 7, ActiveRecord::Migrator::current_version
+ ensure
+ ActiveRecord::Base.table_name_prefix = old_table_name_prefix
+ ActiveRecord::SchemaMigration.table_name = table_name
+ end
- def test_schema_raises_an_error_for_invalid_column_type
- assert_raise NoMethodError do
- ActiveRecord::Schema.define(version: 8) do
- create_table :vegetables do |t|
- t.unknown :color
- end
+ def test_schema_raises_an_error_for_invalid_column_type
+ assert_raise NoMethodError do
+ ActiveRecord::Schema.define(version: 8) do
+ create_table :vegetables do |t|
+ t.unknown :color
end
end
end
+ end
- def test_schema_subclass
- Class.new(ActiveRecord::Schema).define(version: 9) do
- create_table :fruits
- end
- assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
+ def test_schema_subclass
+ Class.new(ActiveRecord::Schema).define(version: 9) do
+ create_table :fruits
end
+ assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
+ end
- def test_normalize_version
- assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118")
- assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2")
- assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
- assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
- end
+ def test_normalize_version
+ assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118")
+ assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2")
+ assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
+ assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
+ end
- def test_schema_load_with_multiple_indexes_for_column_of_different_names
- ActiveRecord::Schema.define do
- create_table :multiple_indexes do |t|
- t.string "foo"
- t.index ["foo"], name: "multiple_indexes_foo_1"
- t.index ["foo"], name: "multiple_indexes_foo_2"
- end
+ def test_schema_load_with_multiple_indexes_for_column_of_different_names
+ ActiveRecord::Schema.define do
+ create_table :multiple_indexes do |t|
+ t.string "foo"
+ t.index ["foo"], name: "multiple_indexes_foo_1"
+ t.index ["foo"], name: "multiple_indexes_foo_2"
end
+ end
- indexes = @connection.indexes("multiple_indexes")
+ indexes = @connection.indexes("multiple_indexes")
- assert_equal 2, indexes.length
- assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
- end
+ assert_equal 2, indexes.length
+ assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
+ end
- def test_timestamps_without_null_set_null_to_false_on_create_table
- ActiveRecord::Schema.define do
- create_table :has_timestamps do |t|
- t.timestamps
- end
+ def test_timestamps_without_null_set_null_to_false_on_create_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
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
end
- def test_timestamps_without_null_set_null_to_false_on_change_table
- ActiveRecord::Schema.define do
- create_table :has_timestamps
+ assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ end
- change_table :has_timestamps do |t|
- t.timestamps default: Time.now
- end
- end
+ def test_timestamps_without_null_set_null_to_false_on_change_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ change_table :has_timestamps do |t|
+ t.timestamps default: Time.now
+ end
end
- def test_timestamps_without_null_set_null_to_false_on_add_timestamps
- ActiveRecord::Schema.define do
- create_table :has_timestamps
- 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
+ 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
+ def test_timestamps_without_null_set_null_to_false_on_add_timestamps
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ 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
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 6b7e4fee56..5b08ba1358 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -116,6 +116,26 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
+ def test_default
+ david = developers(:david)
+ jamis = developers(:jamis)
+
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "ships"
+ def self.name; "Temp"; end
+ belongs_to :developer, default: -> { david }
+ end
+
+ ship = model.create!
+ assert_equal david, ship.developer
+
+ ship = model.create!(developer: jamis)
+ assert_equal jamis, ship.developer
+
+ ship.update!(developer: nil)
+ assert_equal david, ship.developer
+ end
+
def test_default_scope_on_relations_is_not_cached
counter = 0
@@ -291,6 +311,16 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert client.account.new_record?
end
+ def test_reloading_the_belonging_object
+ odegy_account = accounts(:odegy_account)
+
+ assert_equal "Odegy", odegy_account.firm.name
+ Company.where(id: odegy_account.firm_id).update_all(name: "ODEGY")
+ assert_equal "Odegy", odegy_account.firm.name
+
+ assert_equal "ODEGY", odegy_account.reload_firm.name
+ end
+
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
@@ -1047,7 +1077,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
comment.parent = nil
comment.save!
- assert_equal nil, comment.reload.parent
+ assert_nil comment.reload.parent
assert_equal 0, comments(:greetings).reload.children_count
end
@@ -1062,6 +1092,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, parent.reload.children_count
end
+ def test_belongs_to_with_out_of_range_value_assigning
+ model = Class.new(Comment) do
+ def self.name; "Temp"; end
+ validates :post, presence: true
+ end
+
+ comment = model.new
+ comment.post_id = 9223372036854775808 # out of range in the bigint
+
+ assert_nil comment.post
+ assert_not comment.valid?
+ assert_equal [{ error: :blank }], comment.errors.details[:post]
+ end
+
def test_polymorphic_with_custom_primary_key
toy = Toy.create!
sponsor = Sponsor.create!(sponsorable: toy)
@@ -1107,12 +1151,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
Column.create! record: record
assert_equal 1, Column.count
end
-
- def test_association_force_reload_with_only_true_is_deprecated
- client = Client.find(3)
-
- assert_deprecated { client.firm(true) }
- end
end
class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index 5fd2411f6f..7721bd5cd9 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -7,7 +7,7 @@ require "models/computer"
require "models/company"
class AssociationCallbacksTest < ActiveRecord::TestCase
- fixtures :posts, :authors, :projects, :developers
+ fixtures :posts, :authors, :author_addresses, :projects, :developers
def setup
@david = authors(:david)
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index ddb5c7a4aa..3638c87968 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -12,7 +12,7 @@ require "models/vertex"
require "models/edge"
class CascadedEagerLoadingTest < ActiveRecord::TestCase
- fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments,
+ fixtures :authors, :author_addresses, :mixins, :companies, :posts, :topics, :accounts, :comments,
:categorizations, :people, :categories, :edges, :vertices
def test_eager_association_loading_with_cascaded_two_levels
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index ed1b0f5226..e9f551b6b2 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -12,7 +12,7 @@ module Remembered
included do
after_create :remember
- protected
+ private
def remember; self.class.remembered << self; end
end
diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb
index 5d1c1c4b9b..16eff15026 100644
--- a/activerecord/test/cases/associations/eager_singularization_test.rb
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -1,147 +1,146 @@
require "cases/helper"
-if ActiveRecord::Base.connection.supports_migrations?
- class EagerSingularizationTest < ActiveRecord::TestCase
- class Virus < ActiveRecord::Base
- belongs_to :octopus
- end
-
- class Octopus < ActiveRecord::Base
- has_one :virus
- end
-
- class Pass < ActiveRecord::Base
- belongs_to :bus
- end
-
- class Bus < ActiveRecord::Base
- has_many :passes
- end
-
- class Mess < ActiveRecord::Base
- has_and_belongs_to_many :crises
- end
-
- class Crisis < ActiveRecord::Base
- has_and_belongs_to_many :messes
- has_many :analyses, dependent: :destroy
- has_many :successes, through: :analyses
- has_many :dresses, dependent: :destroy
- has_many :compresses, through: :dresses
- end
-
- class Analysis < ActiveRecord::Base
- belongs_to :crisis
- belongs_to :success
- end
-
- class Success < ActiveRecord::Base
- has_many :analyses, dependent: :destroy
- has_many :crises, through: :analyses
- end
-
- class Dress < ActiveRecord::Base
- belongs_to :crisis
- has_many :compresses
- end
-
- class Compress < ActiveRecord::Base
- belongs_to :dress
- end
-
- def setup
- connection.create_table :viri do |t|
- t.column :octopus_id, :integer
- t.column :species, :string
- end
- connection.create_table :octopi do |t|
- t.column :species, :string
- end
- connection.create_table :passes do |t|
- t.column :bus_id, :integer
- t.column :rides, :integer
- end
- connection.create_table :buses do |t|
- t.column :name, :string
- end
- connection.create_table :crises_messes, id: false do |t|
- t.column :crisis_id, :integer
- t.column :mess_id, :integer
- end
- connection.create_table :messes do |t|
- t.column :name, :string
- end
- connection.create_table :crises do |t|
- t.column :name, :string
- end
- connection.create_table :successes do |t|
- t.column :name, :string
- end
- connection.create_table :analyses do |t|
- t.column :crisis_id, :integer
- t.column :success_id, :integer
- end
- connection.create_table :dresses do |t|
- t.column :crisis_id, :integer
- end
- connection.create_table :compresses do |t|
- t.column :dress_id, :integer
- end
- end
-
- teardown do
- connection.drop_table :viri
- connection.drop_table :octopi
- connection.drop_table :passes
- connection.drop_table :buses
- connection.drop_table :crises_messes
- connection.drop_table :messes
- connection.drop_table :crises
- connection.drop_table :successes
- connection.drop_table :analyses
- connection.drop_table :dresses
- connection.drop_table :compresses
- end
+class EagerSingularizationTest < ActiveRecord::TestCase
+ class Virus < ActiveRecord::Base
+ belongs_to :octopus
+ end
- def connection
- ActiveRecord::Base.connection
+ class Octopus < ActiveRecord::Base
+ has_one :virus
+ end
+
+ class Pass < ActiveRecord::Base
+ belongs_to :bus
+ end
+
+ class Bus < ActiveRecord::Base
+ has_many :passes
+ end
+
+ class Mess < ActiveRecord::Base
+ has_and_belongs_to_many :crises
+ end
+
+ class Crisis < ActiveRecord::Base
+ has_and_belongs_to_many :messes
+ has_many :analyses, dependent: :destroy
+ has_many :successes, through: :analyses
+ has_many :dresses, dependent: :destroy
+ has_many :compresses, through: :dresses
+ end
+
+ class Analysis < ActiveRecord::Base
+ belongs_to :crisis
+ belongs_to :success
+ end
+
+ class Success < ActiveRecord::Base
+ has_many :analyses, dependent: :destroy
+ has_many :crises, through: :analyses
+ end
+
+ class Dress < ActiveRecord::Base
+ belongs_to :crisis
+ has_many :compresses
+ end
+
+ class Compress < ActiveRecord::Base
+ belongs_to :dress
+ end
+
+ def setup
+ connection.create_table :viri do |t|
+ t.column :octopus_id, :integer
+ t.column :species, :string
end
+ connection.create_table :octopi do |t|
+ t.column :species, :string
+ end
+ connection.create_table :passes do |t|
+ t.column :bus_id, :integer
+ t.column :rides, :integer
+ end
+ connection.create_table :buses do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises_messes, id: false do |t|
+ t.column :crisis_id, :integer
+ t.column :mess_id, :integer
+ end
+ connection.create_table :messes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises do |t|
+ t.column :name, :string
+ end
+ connection.create_table :successes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :analyses do |t|
+ t.column :crisis_id, :integer
+ t.column :success_id, :integer
+ end
+ connection.create_table :dresses do |t|
+ t.column :crisis_id, :integer
+ end
+ connection.create_table :compresses do |t|
+ t.column :dress_id, :integer
+ end
+ end
- def test_eager_no_extra_singularization_belongs_to
- assert_nothing_raised do
- Virus.all.merge!(includes: :octopus).to_a
- end
+ teardown do
+ connection.drop_table :viri
+ connection.drop_table :octopi
+ connection.drop_table :passes
+ connection.drop_table :buses
+ connection.drop_table :crises_messes
+ connection.drop_table :messes
+ connection.drop_table :crises
+ connection.drop_table :successes
+ connection.drop_table :analyses
+ connection.drop_table :dresses
+ connection.drop_table :compresses
+ end
+
+ def test_eager_no_extra_singularization_belongs_to
+ assert_nothing_raised do
+ Virus.all.merge!(includes: :octopus).to_a
end
+ end
- def test_eager_no_extra_singularization_has_one
- assert_nothing_raised do
- Octopus.all.merge!(includes: :virus).to_a
- end
+ def test_eager_no_extra_singularization_has_one
+ assert_nothing_raised do
+ Octopus.all.merge!(includes: :virus).to_a
end
+ end
- def test_eager_no_extra_singularization_has_many
- assert_nothing_raised do
- Bus.all.merge!(includes: :passes).to_a
- end
+ def test_eager_no_extra_singularization_has_many
+ assert_nothing_raised do
+ Bus.all.merge!(includes: :passes).to_a
end
+ end
- def test_eager_no_extra_singularization_has_and_belongs_to_many
- assert_nothing_raised do
- Crisis.all.merge!(includes: :messes).to_a
- Mess.all.merge!(includes: :crises).to_a
- end
+ def test_eager_no_extra_singularization_has_and_belongs_to_many
+ assert_nothing_raised do
+ Crisis.all.merge!(includes: :messes).to_a
+ Mess.all.merge!(includes: :crises).to_a
end
+ end
- def test_eager_no_extra_singularization_has_many_through_belongs_to
- assert_nothing_raised do
- Crisis.all.merge!(includes: :successes).to_a
- end
+ def test_eager_no_extra_singularization_has_many_through_belongs_to
+ assert_nothing_raised do
+ Crisis.all.merge!(includes: :successes).to_a
end
+ end
- def test_eager_no_extra_singularization_has_many_through_has_many
- assert_nothing_raised do
- Crisis.all.merge!(includes: :compresses).to_a
- end
+ def test_eager_no_extra_singularization_has_many_through_has_many
+ assert_nothing_raised do
+ Crisis.all.merge!(includes: :compresses).to_a
end
end
+
+ private
+ def connection
+ ActiveRecord::Base.connection
+ end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index dc04ccdccc..11f4aae5b3 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -241,7 +241,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
post = assert_queries(1) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) }
# find the post, then find the author which is null so no query for the author or address
assert_no_queries do
- assert_equal nil, post.author_with_address
+ assert_nil post.author_with_address
end
end
@@ -250,7 +250,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
sponsor.update!(sponsorable: nil)
sponsor = assert_queries(1) { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) }
assert_no_queries do
- assert_equal nil, sponsor.sponsorable
+ assert_nil sponsor.sponsorable
end
end
@@ -261,7 +261,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nothing_raised { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) }
end
assert_no_queries do
- assert_equal nil, sponsor.sponsorable
+ assert_nil sponsor.sponsorable
end
end
@@ -739,18 +739,25 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_invalid_association_reference
- assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
Post.all.merge!(includes: :monkeys).find(6)
}
- assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
Post.all.merge!(includes: [ :monkeys ]).find(6)
}
- assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
Post.all.merge!(includes: [ "monkeys" ]).find(6)
}
- assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
+
+ e = assert_raise(ActiveRecord::AssociationNotFoundError) {
Post.all.merge!(includes: [ :monkeys, :elephants ]).find(6)
}
+ assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message)
end
def test_eager_has_many_through_with_order
@@ -933,7 +940,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
d2 = find_all_ordered(Firm, :account)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
- assert_equal(d1[i].account, d2[i].account)
+ if d1[i].account.nil?
+ assert_nil(d2[i].account)
+ else
+ assert_equal(d1[i].account, d2[i].account)
+ end
end
end
@@ -943,7 +954,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
d2 = find_all_ordered(Client, firm_types)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
- firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) }
+ firm_types.each do |type|
+ if (expected = d1[i].send(type)).nil?
+ assert_nil(d2[i].send(type))
+ else
+ assert_equal(expected, d2[i].send(type))
+ end
+ end
end
end
def test_eager_with_valid_association_as_string_not_symbol
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index 974a3080d4..87d842f21d 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -36,6 +36,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent
end
+ def test_extension_with_dirty_target
+ comment = posts(:welcome).comments.build(body: "New comment")
+ assert_equal comment, posts(:welcome).comments.with_content("New comment")
+ end
+
def test_marshalling_extensions
david = developers(:david)
assert_equal projects(:action_controller), david.projects.find_most_recent
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 4b7ac594cf..d6b595d7e7 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
@@ -86,8 +86,10 @@ class DeveloperWithSymbolClassName < Developer
has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys
end
-class DeveloperWithConstantClassName < Developer
- has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys
+ActiveSupport::Deprecation.silence do
+ class DeveloperWithConstantClassName < Developer
+ has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys
+ end
end
class DeveloperWithExtendOption < Developer
@@ -743,8 +745,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped_having
- assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size
- assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 }
+ assert_equal 2, projects(:active_record).well_paid_salary_groups.to_a.size
+ assert projects(:active_record).well_paid_salary_groups.all? { |g| g.salary > 10000 }
end
def test_get_ids
@@ -955,12 +957,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_association_force_reload_with_only_true_is_deprecated
- developer = Developer.find(1)
-
- assert_deprecated { developer.projects(true) }
- 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 d657be71cc..e2f044c139 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -40,7 +40,7 @@ require "models/zine"
require "models/interest"
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
- fixtures :authors, :posts, :comments
+ fixtures :authors, :author_addresses, :posts, :comments
def test_should_generate_valid_sql
author = authors(:david)
@@ -51,7 +51,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
end
class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
- fixtures :authors, :essays, :subscribers, :subscriptions, :people
+ fixtures :authors, :author_addresses, :essays, :subscribers, :subscriptions, :people
def test_custom_primary_key_on_new_record_should_fetch_with_query
subscriber = Subscriber.new(nick: "webster132")
@@ -100,7 +100,7 @@ end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
- :developers_projects, :topics, :authors, :comments,
+ :developers_projects, :topics, :authors, :author_addresses, :comments,
:posts, :readers, :taggings, :cars, :jobs, :tags,
:categorizations, :zines, :interests
@@ -187,7 +187,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
ship.parts.clear
part.reload
- assert_equal nil, part.ship
+ assert_nil part.ship
assert !part.updated_at_changed?
end
@@ -611,21 +611,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_update_all_on_association_accessed_before_save
firm = Firm.new(name: "Firm")
- clients_proxy_id = firm.clients.object_id
firm.clients << Client.first
firm.save!
assert_equal firm.clients.count, firm.clients.update_all(description: "Great!")
- assert_not_equal clients_proxy_id, firm.clients.object_id
end
def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key
- # We can use the same cached proxy object because the id is available for the scope
firm = Firm.new(name: "Firm", id: 100)
- clients_proxy_id = firm.clients.object_id
firm.clients << Client.first
firm.save!
assert_equal firm.clients.count, firm.clients.update_all(description: "Great!")
- assert_equal clients_proxy_id, firm.clients.object_id
end
def test_belongs_to_sanity
@@ -788,11 +783,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id)
end
- def test_select_with_block_and_specific_attributes
- assert_deprecated do
- comments = posts(:welcome).comments.select(:id, :body) { |c| c.id == 1 }
- assert_equal [1], comments.map(&:id)
- end
+ def test_select_with_block_and_dirty_target
+ assert_equal 2, posts(:welcome).comments.select { true }.size
+ posts(:welcome).comments.build
+ assert_equal 3, posts(:welcome).comments.select { true }.size
end
def test_select_without_foreign_key
@@ -1582,26 +1576,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert firm.companies.exists?(name: "child")
end
- def test_restrict_with_error_is_deprecated_using_key_many
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: "message for deprecated key" } } } }
-
- firm = RestrictedWithErrorFirm.create!(name: "restrict")
- firm.companies.create(name: "child")
-
- assert !firm.companies.empty?
-
- assert_deprecated { firm.destroy }
-
- assert !firm.errors.empty?
-
- assert_equal "message for deprecated key", firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(name: "restrict")
- assert firm.companies.exists?(name: "child")
- ensure
- I18n.backend.reload!
- end
-
def test_restrict_with_error
firm = RestrictedWithErrorFirm.create!(name: "restrict")
firm.companies.create(name: "child")
@@ -1929,7 +1903,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_many_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
+ firm.clients.load # force load
assert_no_queries { assert firm.clients.many? }
end
@@ -1968,7 +1942,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_none_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
+ firm.clients.load # force load
assert_no_queries { assert ! firm.clients.none? }
end
@@ -2003,7 +1977,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_one_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
+ firm.clients.load # force load
assert_no_queries { assert ! firm.clients.one? }
end
@@ -2475,19 +2449,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "double insertion of new object to association when same association used in the after create callback of a new object" do
reset_callbacks(:save, Bulb) do
- Bulb.after_save { |record| record.car.bulbs.to_a }
+ Bulb.after_save { |record| record.car.bulbs.load }
car = Car.create!
car.bulbs << Bulb.new
assert_equal 1, car.bulbs.size
end
end
- def test_association_force_reload_with_only_true_is_deprecated
- company = Company.find(1)
-
- assert_deprecated { company.clients_of_firm(true) }
- 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 c2239ac03a..ea52fb5a67 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -28,6 +28,9 @@ require "models/member"
require "models/membership"
require "models/club"
require "models/organization"
+require "models/user"
+require "models/family"
+require "models/family_tree"
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
@@ -402,7 +405,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert_equal nil, reference.reload.job_id
+ assert_nil reference.reload.job_id
ensure
Reference.make_comments = false
end
@@ -423,7 +426,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
# Check that the destroy callback on Reference did not run
- assert_equal nil, person.reload.comments
+ assert_nil person.reload.comments
ensure
Reference.make_comments = false
end
@@ -485,7 +488,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
references.each do |reference|
- assert_equal nil, reference.reload.job_id
+ assert_nil reference.reload.job_id
end
end
@@ -880,13 +883,34 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
book.subscriber_ids = []
assert_equal [], book.subscribers.reload
end
+ end
+ def test_collection_singular_ids_setter_with_changed_primary_key
+ company = companies(:first_firm)
+ client = companies(:first_client)
+ company.clients_using_primary_key_ids = [client.name]
+ assert_equal [client], company.clients_using_primary_key
end
def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set
company = companies(:rails_core)
ids = [Developer.first.id, -9999]
- assert_raises(ActiveRecord::AssociationTypeMismatch) { company.developer_ids = ids }
+ e = assert_raises(ActiveRecord::RecordNotFound) { company.developer_ids = ids }
+ assert_match(/Couldn't find all Developers with 'id'/, e.message)
+ end
+
+ def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set_with_changed_primary_key
+ company = companies(:first_firm)
+ ids = [Client.first.name, "unknown client"]
+ e = assert_raises(ActiveRecord::RecordNotFound) { company.clients_using_primary_key_ids = ids }
+ assert_match(/Couldn't find all Clients with 'name'/, e.message)
+ end
+
+ def test_collection_singular_ids_through_setter_raises_exception_when_invalid_ids_set
+ author = authors(:david)
+ ids = [categories(:general).name, "Unknown"]
+ e = assert_raises(ActiveRecord::RecordNotFound) { author.essay_category_ids = ids }
+ assert_equal "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2)", e.message
end
def test_build_a_model_from_hm_through_association_with_where_clause
@@ -1182,12 +1206,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_nil Club.new.special_favourites.distinct_value
end
- def test_association_force_reload_with_only_true_is_deprecated
- post = Post.find(1)
-
- assert_deprecated { post.people(true) }
- end
-
def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
member = Member.create!
club = Club.create!
@@ -1215,4 +1233,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
ensure
TenantMembership.current_member = nil
end
+
+ def test_has_many_through_with_scope_should_respect_table_alias
+ family = Family.create!
+ users = 3.times.map { User.create! }
+ FamilyTree.create!(member: users[0], family: family)
+ FamilyTree.create!(member: users[1], family: family)
+ FamilyTree.create!(member: users[2], family: family, token: "wat")
+
+ assert_equal 2, users[0].family_members.to_a.size
+ assert_equal 0, users[2].family_members.to_a.size
+ end
+
+ def test_incorrectly_ordered_through_associations
+ assert_raises(ActiveRecord::HasManyThroughOrderError) do
+ DeveloperWithIncorrectlyOrderedHasManyThrough.create(
+ companies: [Company.create]
+ )
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 862f33a1a0..7c11d2e7fc 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -186,25 +186,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert firm.account.present?
end
- def test_restrict_with_error_is_deprecated_using_key_one
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: "message for deprecated key" } } } }
-
- firm = RestrictedWithErrorFirm.create!(name: "restrict")
- firm.create_account(credit_limit: 10)
-
- assert_not_nil firm.account
-
- assert_deprecated { firm.destroy }
-
- assert !firm.errors.empty?
- assert_equal "message for deprecated key", firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(name: "restrict")
- assert firm.account.present?
- ensure
- I18n.backend.reload!
- end
-
def test_restrict_with_error
firm = RestrictedWithErrorFirm.create!(name: "restrict")
firm.create_account(credit_limit: 10)
@@ -326,6 +307,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_reload_association
+ odegy = companies(:odegy)
+
+ assert_equal 53, odegy.account.credit_limit
+ Account.where(id: odegy.account.id).update_all(credit_limit: 80)
+ assert_equal 53, odegy.account.credit_limit
+
+ assert_equal 80, odegy.reload_account.credit_limit
+ end
+
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -485,7 +476,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal ships(:black_pearl), pirate.ship
assert_equal pirate.id, pirate.ship.pirate_id
- assert_equal "Failed to remove the existing associated ship. " +
+ assert_equal "Failed to remove the existing associated ship. " \
"The record failed to save after its foreign key was set to nil.", error.message
end
@@ -654,15 +645,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
end
- def test_association_force_reload_with_only_true_is_deprecated
- firm = Firm.find(1)
-
- assert_deprecated { firm.account(true) }
- end
-
class SpecialBook < ActiveRecord::Base
self.table_name = "books"
belongs_to :author, class_name: "SpecialAuthor"
+ has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id"
end
class SpecialAuthor < ActiveRecord::Base
@@ -670,11 +656,27 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
has_one :book, class_name: "SpecialBook", foreign_key: "author_id"
end
- def test_assocation_enum_works_properly
+ class SpecialSupscription < ActiveRecord::Base
+ self.table_name = "subscriptions"
+ belongs_to :book, class_name: "SpecialBook"
+ end
+
+ def test_association_enum_works_properly
author = SpecialAuthor.create!(name: "Test")
book = SpecialBook.create!(status: "published")
author.book = book
refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
end
+
+ def test_association_enum_works_properly_with_nested_join
+ author = SpecialAuthor.create!(name: "Test")
+ book = SpecialBook.create!(status: "published")
+ author.book = book
+
+ where_clause = { books: { subscriptions: { subscriber_id: nil } } }
+ assert_nothing_raised do
+ SpecialAuthor.joins(book: :subscription).where.not(where_clause)
+ 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 6ba062a248..28b883586d 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -23,7 +23,7 @@ require "models/customer_carrier"
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
- :dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners
+ :dashboards, :speedometers, :authors, :author_addresses, :posts, :comments, :categories, :essays, :owners
def setup
@member = members(:groucho)
@@ -82,10 +82,17 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_set_record_to_nil_should_delete_association
@member.club = nil
@member.reload
- assert_equal nil, @member.current_membership
+ assert_nil @member.current_membership
assert_nil @member.club
end
+ def test_set_record_after_delete_association
+ @member.club = nil
+ @member.club = clubs(:moustache_club)
+ @member.reload
+ assert_equal clubs(:moustache_club), @member.club
+ end
+
def test_has_one_through_polymorphic
assert_equal clubs(:moustache_club), @member.sponsor_club
end
@@ -110,12 +117,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
# conditions on the through table
assert_equal clubs(:moustache_club), Member.all.merge!(includes: :favourite_club).find(@member.id).favourite_club
memberships(:membership_of_favourite_club).update_columns(favourite: false)
- assert_equal nil, Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club
+ assert_nil Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club
# conditions on the source table
assert_equal clubs(:moustache_club), Member.all.merge!(includes: :hairy_club).find(@member.id).hairy_club
clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons")
- assert_equal nil, Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club
+ assert_nil Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club
end
def test_has_one_through_polymorphic_with_source_type
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 7414869c8f..ddf5bc6f0b 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -10,7 +10,7 @@ require "models/tagging"
require "models/tag"
class InnerJoinAssociationTest < ActiveRecord::TestCase
- fixtures :authors, :essays, :posts, :comments, :categories, :categories_posts, :categorizations,
+ fixtures :authors, :author_addresses, :essays, :posts, :comments, :categories, :categories_posts, :categorizations,
:taggings, :tags
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 6fe6ee6783..287b3e9ebc 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -443,7 +443,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held"
assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
- assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed"
+ assert_nil man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed"
man.name = "Ben Bitdiddle"
assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed"
man.interests.find(interest.id).man.name = "Alyssa P. Hacker"
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index a4345f3857..c078cef064 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -19,7 +19,7 @@ require "models/car"
class AssociationsJoinModelTest < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
- fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books,
+ fixtures :posts, :authors, :author_addresses, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books,
# Reload edges table from fixtures as otherwise repeated test was failing
:edges
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 42dbbad1c8..2aca3523c4 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -7,7 +7,7 @@ require "models/categorization"
require "models/person"
class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
- fixtures :authors, :essays, :posts, :comments, :categorizations, :people
+ fixtures :authors, :essays, :posts, :comments, :categorizations, :people, :author_addresses
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index dc26f6a383..67ff7355b3 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -24,7 +24,7 @@ require "models/membership"
require "models/essay"
class NestedThroughAssociationsTest < ActiveRecord::TestCase
- fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
+ fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
:people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details,
:member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts,
:categorizations, :memberships, :essays
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
index f8b686721e..45e1803858 100644
--- a/activerecord/test/cases/associations/required_test.rb
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -22,14 +22,21 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
@connection.drop_table "children", if_exists: true
end
- test "belongs_to associations are not required by default" do
- model = subclass_of(Child) do
- belongs_to :parent, inverse_of: false,
- class_name: "RequiredAssociationsTest::Parent"
- end
+ test "belongs_to associations can be optional by default" do
+ begin
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = false
+
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
- assert model.new.save
- assert model.new(parent: Parent.new).save
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
end
test "required belongs_to associations have presence validated" do
@@ -46,6 +53,27 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
assert record.save
end
+ test "belongs_to associations can be required by default" do
+ begin
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
+
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent must exist"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
+ end
+ end
+
test "has_one associations are not required by default" do
model = subclass_of(Parent) do
has_one :child, inverse_of: false,
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index c095b3a91c..4ab690bfc6 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -22,7 +22,7 @@ require "models/interest"
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
- :computers, :people, :readers, :authors, :author_favorites
+ :computers, :people, :readers, :authors, :author_addresses, :author_favorites
def test_eager_loading_should_not_change_count_of_children
liquid = Liquid.create(name: "salty")
@@ -88,10 +88,10 @@ class AssociationsTest < ActiveRecord::TestCase
assert firm.clients.empty?, "New firm should have cached no client objects"
assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
- ActiveSupport::Deprecation.silence do
- assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
- assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
- end
+ firm.clients.reload
+
+ assert !firm.clients.empty?, "New firm should have reloaded client objects"
+ assert_equal 1, firm.clients.size, "New firm should have reloaded clients count"
end
def test_using_limitable_reflections_helper
@@ -104,19 +104,6 @@ class AssociationsTest < ActiveRecord::TestCase
assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
end
- def test_force_reload_is_uncached
- firm = Firm.create!("name" => "A New Firm, Inc")
- Client.create!("name" => "TheClient.com", :firm => firm)
-
- ActiveSupport::Deprecation.silence do
- ActiveRecord::Base.cache do
- firm.clients.each {}
- assert_queries(0) { assert_not_nil firm.clients.each {} }
- assert_queries(1) { assert_not_nil firm.clients(true).each {} }
- end
- end
- end
-
def test_association_with_references
firm = companies(:first_firm)
assert_includes firm.association_with_references.references_values, "foo"
@@ -124,7 +111,7 @@ class AssociationsTest < ActiveRecord::TestCase
end
class AssociationProxyTest < ActiveRecord::TestCase
- fixtures :authors, :posts, :categorizations, :categories, :developers, :projects, :developers_projects
+ fixtures :authors, :author_addresses, :posts, :categorizations, :categories, :developers, :projects, :developers_projects
def test_push_does_not_load_target
david = authors(:david)
@@ -233,11 +220,6 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal david.projects, david.projects.scope
end
- test "proxy object is cached" do
- david = developers(:david)
- assert david.projects.equal?(david.projects)
- end
-
test "inverses get set of subsets of the association" do
man = Man.create
man.interests.create
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 978dd93fa4..1fc63a49d4 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -3,7 +3,7 @@ require "cases/helper"
module ActiveRecord
module AttributeMethods
class ReadTest < ActiveRecord::TestCase
- class FakeColumn < Struct.new(:name)
+ FakeColumn = Struct.new(:name) do
def type; :integer; end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index ba6877a6a6..4d24a980dc 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -92,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "attribute keys on a new instance" do
t = Topic.new
- assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
+ assert_nil t.title, "The topics table has a title column, so it should be nil"
assert_raise(NoMethodError) { t.title2 }
end
@@ -156,7 +156,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
keyboard = Keyboard.create
keyboard.key_number = "10"
assert_equal "10", keyboard.id_before_type_cast
- assert_equal nil, keyboard.read_attribute_before_type_cast("id")
+ assert_nil keyboard.read_attribute_before_type_cast("id")
assert_equal "10", keyboard.read_attribute_before_type_cast("key_number")
assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number)
end
@@ -213,7 +213,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record.written_on = "345643456"
assert_equal "345643456", record.written_on_before_type_cast
- assert_equal nil, record.written_on
+ assert_nil record.written_on
record.written_on = "2009-10-11 12:13:14"
assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast
@@ -319,6 +319,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "Still another topic: part 4", topic.title
end
+ test "write_attribute can write aliased attributes as well" do
+ topic = Topic.new(title: "Don't change the topic")
+ topic.write_attribute :heading, "New topic"
+
+ assert_equal "New topic", topic.title
+ end
+
test "read_attribute" do
topic = Topic.new
topic.title = "Don't change the topic"
@@ -329,6 +336,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "Don't change the topic", topic[:title]
end
+ test "read_attribute can read aliased attributes as well" do
+ topic = Topic.new(title: "Don't change the topic")
+
+ assert_equal "Don't change the topic", topic.read_attribute("heading")
+ assert_equal "Don't change the topic", topic["heading"]
+
+ assert_equal "Don't change the topic", topic.read_attribute(:heading)
+ assert_equal "Don't change the topic", topic[:heading]
+ end
+
test "read_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do
computer = Computer.select("id").first
assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
@@ -737,7 +754,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "time zone-aware attributes do not recurse infinitely on invalid values" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: [])
- assert_equal nil, record.bonus_time
+ assert_nil record.bonus_time
end
end
@@ -849,6 +866,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert subklass.method_defined?(:id), "subklass is missing id method"
end
+ test "define_attribute_method works with both symbol and string" do
+ klass = Class.new(ActiveRecord::Base)
+
+ assert_nothing_raised { klass.define_attribute_method(:foo) }
+ assert_nothing_raised { klass.define_attribute_method("bar") }
+ end
+
test "read_attribute with nil should not asplode" do
assert_nil Topic.new.read_attribute(nil)
end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index 059b5b2401..bd4b200735 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -25,7 +25,7 @@ module ActiveRecord
attributes = builder.build_from_database(foo: "3.3")
assert_equal "3.3", attributes[:foo].value_before_type_cast
- assert_equal nil, attributes[:bar].value_before_type_cast
+ assert_nil attributes[:bar].value_before_type_cast
assert_equal :bar, attributes[:bar].name
end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index f4620ae2da..3705a6be89 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -255,5 +255,13 @@ module ActiveRecord
assert_includes inspection, "non_existent_decimal"
end
+
+ test "attributes do not require a type" do
+ klass = Class.new(OverloadedType) do
+ attribute :no_type
+ end
+ assert_equal 1, klass.new(no_type: 1).no_type
+ assert_equal "foo", klass.new(no_type: "foo").no_type
+ end
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index a3f82ed49d..2203aa1788 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -36,11 +36,11 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
private
- def should_be_cool
- unless self.first_name == "cool"
- errors.add :first_name, "not cool"
+ def should_be_cool
+ unless first_name == "cool"
+ errors.add :first_name, "not cool"
+ end
end
- end
}
reference = Class.new(ActiveRecord::Base) {
self.table_name = "references"
@@ -1391,6 +1391,14 @@ module AutosaveAssociationOnACollectionAssociationTests
assert_equal "Squawky", parrot.reload.name
end
+ def test_should_not_update_children_when_parent_creation_with_no_reason
+ parrot = Parrot.create!(name: "Polly")
+ assert_equal 0, parrot.updated_count
+
+ Pirate.create!(parrot_ids: [parrot.id], catchphrase: "Arrrr")
+ assert_equal 0, parrot.reload.updated_count
+ end
+
def test_should_automatically_validate_the_associated_models
@pirate.send(@association_name).each { |child| child.name = "" }
@@ -1699,3 +1707,27 @@ class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
assert_nothing_raised { invoice.line_items.create(amount: 10) }
end
end
+
+class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::TestCase
+ class Post < ActiveRecord::Base
+ has_many :comments, inverse_of: :post
+ end
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post, inverse_of: :comments
+
+ attr_accessor :post_comments_count
+ after_save do
+ self.post_comments_count = post.comments.count
+ end
+ end
+
+ def test_after_save_callback_with_autosave
+ post = Post.new(title: "Test", body: "...")
+ comment = post.comments.build(body: "...")
+ post.save!
+
+ assert_equal 1, post.comments.count
+ assert_equal 1, comment.post_comments_count
+ end
+end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 4e9d78de5d..ee1be09358 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -4,6 +4,7 @@ require "models/author"
require "models/topic"
require "models/reply"
require "models/category"
+require "models/categorization"
require "models/company"
require "models/customer"
require "models/developer"
@@ -33,8 +34,6 @@ class SecondAbstractClass < FirstAbstractClass
self.abstract_class = true
end
class Photo < SecondAbstractClass; end
-class Category < ActiveRecord::Base; end
-class Categorization < ActiveRecord::Base; end
class Smarts < ActiveRecord::Base; end
class CreditCard < ActiveRecord::Base
class PinNumber < ActiveRecord::Base
@@ -45,8 +44,6 @@ class CreditCard < ActiveRecord::Base
class Brand < Category; end
end
class MasterCreditCard < ActiveRecord::Base; end
-class Post < ActiveRecord::Base; end
-class Computer < ActiveRecord::Base; end
class NonExistentTable < ActiveRecord::Base; end
class TestOracleDefault < ActiveRecord::Base; end
@@ -56,12 +53,6 @@ end
class Weird < ActiveRecord::Base; end
-class Boolean < ActiveRecord::Base
- def has_fun
- super
- end
-end
-
class LintTest < ActiveRecord::TestCase
include ActiveModel::Lint::Tests
@@ -73,7 +64,7 @@ class LintTest < ActiveRecord::TestCase
end
class BasicsTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :categorizations, :categories, :posts
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
@@ -107,12 +98,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil Edge.primary_key
end
- unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter)
- def test_limit_with_comma
- assert_deprecated do
- assert Topic.limit("1,2").to_a
- end
- end
+ def test_primary_key_and_references_columns_should_be_identical_type
+ pk = Author.columns_hash["id"]
+ ref = Post.columns_hash["author_id"]
+
+ assert_equal pk.bigint?, ref.bigint?
end
def test_many_mutations
@@ -144,10 +134,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_limit_should_sanitize_sql_injection_for_limit_with_commas
- assert_deprecated do
- assert_raises(ArgumentError) do
- Topic.limit("1, 7 procedure help()").to_a
- end
+ assert_raises(ArgumentError) do
+ Topic.limit("1, 7 procedure help()").to_a
end
end
@@ -622,7 +610,7 @@ class BasicsTest < ActiveRecord::TestCase
Topic.find(topic.id).destroy
end
- assert_equal nil, Topic.find_by_id(topic.id)
+ assert_nil Topic.find_by_id(topic.id)
end
def test_comparison_with_different_objects
@@ -1379,12 +1367,6 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_uniq_delegates_to_scoped
- assert_deprecated do
- assert_equal Bird.all.distinct, Bird.uniq
- end
- end
-
def test_distinct_delegates_to_scoped
assert_equal Bird.all.distinct, Bird.distinct
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 98d202dd79..6032aa9250 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -3,36 +3,36 @@ require "models/topic"
require "models/author"
require "models/post"
-module ActiveRecord
- class BindParameterTest < ActiveRecord::TestCase
- fixtures :topics, :authors, :posts
-
- class LogListener
- attr_accessor :calls
-
- def initialize
- @calls = []
+if ActiveRecord::Base.connection.supports_statement_cache? &&
+ ActiveRecord::Base.connection.prepared_statements
+ module ActiveRecord
+ class BindParameterTest < ActiveRecord::TestCase
+ fixtures :topics, :authors, :author_addresses, :posts
+
+ class LogListener
+ attr_accessor :calls
+
+ def initialize
+ @calls = []
+ end
+
+ def call(*args)
+ calls << args
+ end
end
- def call(*args)
- calls << args
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ @subscriber = LogListener.new
+ @pk = Topic.columns_hash[Topic.primary_key]
+ @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
end
- end
- def setup
- super
- @connection = ActiveRecord::Base.connection
- @subscriber = LogListener.new
- @pk = Topic.columns_hash[Topic.primary_key]
- @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
- end
-
- teardown do
- ActiveSupport::Notifications.unsubscribe(@subscription)
- end
+ def teardown
+ ActiveSupport::Notifications.unsubscribe(@subscription)
+ end
- if ActiveRecord::Base.connection.supports_statement_cache? &&
- ActiveRecord::Base.connection.prepared_statements
def test_bind_from_join_in_subquery
subquery = Author.joins(:thinking_posts).where(name: "David")
scope = Author.from(subquery, "authors").where(id: 1)
@@ -56,43 +56,48 @@ module ActiveRecord
assert message, "expected a message with binds"
end
- def test_logs_bind_vars_after_type_cast
+ def test_logs_binds_after_type_cast
binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
- type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- payload = {
- name: "SQL",
- sql: "select * from topics where id = ?",
- binds: binds,
- type_casted_binds: type_casted_binds
- }
- event = ActiveSupport::Notifications::Event.new(
- "foo",
- Time.now,
- Time.now,
- 123,
- payload)
-
- logger = Class.new(ActiveRecord::LogSubscriber) {
- attr_reader :debugs
- def initialize
- super
- @debugs = []
- end
-
- def debug(str)
- @debugs << str
- end
- }.new
-
- logger.sql event
- assert_match([[@pk.name, 10]].inspect, logger.debugs.first)
+ assert_logs_binds(binds)
end
- private
-
- def type_cast(value)
- ActiveRecord::Base.connection.type_cast(value)
+ def test_logs_legacy_binds_after_type_cast
+ binds = [[@pk, "10"]]
+ assert_logs_binds(binds)
end
+
+ private
+ def assert_logs_binds(binds)
+ payload = {
+ name: "SQL",
+ sql: "select * from topics where id = ?",
+ binds: binds,
+ type_casted_binds: @connection.type_casted_binds(binds)
+ }
+
+ event = ActiveSupport::Notifications::Event.new(
+ "foo",
+ Time.now,
+ Time.now,
+ 123,
+ payload)
+
+ logger = Class.new(ActiveRecord::LogSubscriber) {
+ attr_reader :debugs
+
+ def initialize
+ super
+ @debugs = []
+ end
+
+ def debug(str)
+ @debugs << str
+ end
+ }.new
+
+ logger.sql(event)
+ assert_match([[@pk.name, 10]].inspect, logger.debugs.first)
+ end
end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 87e99fb25c..3214d778d4 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -13,9 +13,9 @@ require "models/speedometer"
require "models/ship_part"
require "models/treasure"
require "models/developer"
+require "models/post"
require "models/comment"
require "models/rating"
-require "models/post"
class NumericData < ActiveRecord::Base
self.table_name = "numeric_data"
@@ -166,14 +166,14 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_limit_should_apply_before_count
- accounts = Account.limit(3).where("firm_id IS NOT NULL")
+ accounts = Account.limit(4)
assert_equal 3, accounts.count(:firm_id)
assert_equal 3, accounts.select(:firm_id).count
end
def test_limit_should_apply_before_count_arel_attribute
- accounts = Account.limit(3).where("firm_id IS NOT NULL")
+ accounts = Account.limit(4)
firm_id_attribute = Account.arel_table[:firm_id]
assert_equal 3, accounts.count(firm_id_attribute)
@@ -227,6 +227,20 @@ class CalculationsTest < ActiveRecord::TestCase
assert_match "credit_limit, firm_name", e.message
end
+ def test_apply_distinct_in_count
+ queries = assert_sql do
+ Account.distinct.count
+ Account.group(:firm_id).distinct.count
+ end
+
+ queries.each do |query|
+ # `table_alias_length` in `column_alias_for` would execute
+ # "SHOW max_identifier_length" statement in PostgreSQL adapter.
+ next if query == "SHOW max_identifier_length"
+ assert_match %r{\ASELECT(?! DISTINCT) COUNT\(DISTINCT\b}, query
+ end
+ 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]
@@ -235,7 +249,8 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_group_by_summed_field_having_condition_from_select
- c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit)
+ skip unless current_adapter?(:Mysql2Adapter, :SQLite3Adapter)
+ c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("min_credit_limit > 50").sum(:credit_limit)
assert_nil c[1]
assert_equal 60, c[2]
assert_equal 53, c[9]
@@ -421,10 +436,6 @@ class CalculationsTest < ActiveRecord::TestCase
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
-
- assert_deprecated do
- assert_equal 4, Account.select(:credit_limit).uniq.count
- end
end
def test_count_with_aliased_attribute
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 4b517e9d70..b3c86586d0 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -6,10 +6,6 @@ class CallbackDeveloper < ActiveRecord::Base
self.table_name = "developers"
class << self
- def callback_string(callback_method)
- "history << [#{callback_method.to_sym.inspect}, :string]"
- end
-
def callback_proc(callback_method)
Proc.new { |model| model.history << [callback_method, :proc] }
end
@@ -33,7 +29,6 @@ class CallbackDeveloper < ActiveRecord::Base
ActiveRecord::Callbacks::CALLBACKS.each do |callback_method|
next if callback_method.to_s.start_with?("around_")
define_callback_method(callback_method)
- ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) }
send(callback_method, callback_proc(callback_method))
send(callback_method, callback_object(callback_method))
send(callback_method) { |model| model.history << [callback_method, :block] }
@@ -44,11 +39,6 @@ class CallbackDeveloper < ActiveRecord::Base
end
end
-class CallbackDeveloperWithFalseValidation < CallbackDeveloper
- before_validation proc { |model| model.history << [:before_validation, :returning_false]; false }
- before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
-end
-
class CallbackDeveloperWithHaltedValidation < CallbackDeveloper
before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) }
before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
@@ -125,11 +115,11 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base
after_validation :after_validation_on_create_and_update, on: [ :create, :update ]
def before_validation_on_create_and_update
- history << "before_validation_on_#{self.validation_context}".to_sym
+ history << "before_validation_on_#{validation_context}".to_sym
end
def after_validation_on_create_and_update
- history << "after_validation_on_#{self.validation_context}".to_sym
+ history << "after_validation_on_#{validation_context}".to_sym
end
def history
@@ -137,23 +127,6 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base
end
end
-class CallbackCancellationDeveloper < ActiveRecord::Base
- self.table_name = "developers"
-
- attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called
- attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy
-
- before_save { defined?(@cancel_before_save) ? !@cancel_before_save : false }
- before_create { !@cancel_before_create }
- before_update { !@cancel_before_update }
- before_destroy { !@cancel_before_destroy }
-
- after_save { @after_save_called = true }
- after_update { @after_update_called = true }
- after_create { @after_create_called = true }
- after_destroy { @after_destroy_called = true }
-end
-
class CallbackHaltedDeveloper < ActiveRecord::Base
self.table_name = "developers"
@@ -178,7 +151,6 @@ class CallbacksTest < ActiveRecord::TestCase
david = CallbackDeveloper.new
assert_equal [
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
@@ -189,12 +161,10 @@ class CallbacksTest < ActiveRecord::TestCase
david = CallbackDeveloper.find(1)
assert_equal [
[ :after_find, :method ],
- [ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
@@ -206,17 +176,14 @@ class CallbacksTest < ActiveRecord::TestCase
david.valid?
assert_equal [
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
- [ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
- [ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
@@ -228,22 +195,18 @@ class CallbacksTest < ActiveRecord::TestCase
david.valid?
assert_equal [
[ :after_find, :method ],
- [ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
- [ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
- [ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
@@ -254,44 +217,36 @@ class CallbacksTest < ActiveRecord::TestCase
david = CallbackDeveloper.create("name" => "David", "salary" => 1000000)
assert_equal [
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
- [ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
- [ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :before_save, :method ],
- [ :before_save, :string ],
[ :before_save, :proc ],
[ :before_save, :object ],
[ :before_save, :block ],
[ :before_create, :method ],
- [ :before_create, :string ],
[ :before_create, :proc ],
[ :before_create, :object ],
[ :before_create, :block ],
[ :after_create, :method ],
- [ :after_create, :string ],
[ :after_create, :proc ],
[ :after_create, :object ],
[ :after_create, :block ],
[ :after_save, :method ],
- [ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
[ :after_save, :block ],
[ :after_commit, :block ],
[ :after_commit, :object ],
[ :after_commit, :proc ],
- [ :after_commit, :string ],
[ :after_commit, :method ]
], david.history
end
@@ -323,49 +278,40 @@ class CallbacksTest < ActiveRecord::TestCase
david.save
assert_equal [
[ :after_find, :method ],
- [ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
- [ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
- [ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :before_save, :method ],
- [ :before_save, :string ],
[ :before_save, :proc ],
[ :before_save, :object ],
[ :before_save, :block ],
[ :before_update, :method ],
- [ :before_update, :string ],
[ :before_update, :proc ],
[ :before_update, :object ],
[ :before_update, :block ],
[ :after_update, :method ],
- [ :after_update, :string ],
[ :after_update, :proc ],
[ :after_update, :object ],
[ :after_update, :block ],
[ :after_save, :method ],
- [ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
[ :after_save, :block ],
[ :after_commit, :block ],
[ :after_commit, :object ],
[ :after_commit, :proc ],
- [ :after_commit, :string ],
[ :after_commit, :method ]
], david.history
end
@@ -399,29 +345,24 @@ class CallbacksTest < ActiveRecord::TestCase
david.destroy
assert_equal [
[ :after_find, :method ],
- [ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_destroy, :method ],
- [ :before_destroy, :string ],
[ :before_destroy, :proc ],
[ :before_destroy, :object ],
[ :before_destroy, :block ],
[ :after_destroy, :method ],
- [ :after_destroy, :string ],
[ :after_destroy, :proc ],
[ :after_destroy, :object ],
[ :after_destroy, :block ],
[ :after_commit, :block ],
[ :after_commit, :object ],
[ :after_commit, :proc ],
- [ :after_commit, :string ],
[ :after_commit, :method ]
], david.history
end
@@ -431,82 +372,16 @@ class CallbacksTest < ActiveRecord::TestCase
CallbackDeveloper.delete(david.id)
assert_equal [
[ :after_find, :method ],
- [ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
- def test_deprecated_before_save_returning_false
- david = ImmutableDeveloper.find(1)
- assert_deprecated do
- assert david.valid?
- assert !david.save
- exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
- assert_equal exc.record, david
- assert_equal "Failed to save the record", exc.message
- end
-
- david = ImmutableDeveloper.find(1)
- david.salary = 10_000_000
- assert !david.valid?
- assert !david.save
- assert_raise(ActiveRecord::RecordInvalid) { david.save! }
-
- someone = CallbackCancellationDeveloper.find(1)
- someone.cancel_before_save = true
- assert_deprecated do
- assert someone.valid?
- assert !someone.save
- end
- assert_save_callbacks_not_called(someone)
- end
-
- def test_deprecated_before_create_returning_false
- someone = CallbackCancellationDeveloper.new
- someone.cancel_before_create = true
- assert_deprecated do
- assert someone.valid?
- assert !someone.save
- end
- assert_save_callbacks_not_called(someone)
- end
-
- def test_deprecated_before_update_returning_false
- someone = CallbackCancellationDeveloper.find(1)
- someone.cancel_before_update = true
- assert_deprecated do
- assert someone.valid?
- assert !someone.save
- end
- assert_save_callbacks_not_called(someone)
- end
-
- def test_deprecated_before_destroy_returning_false
- david = ImmutableDeveloper.find(1)
- assert_deprecated do
- assert !david.destroy
- exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
- assert_equal exc.record, david
- assert_equal "Failed to destroy the record", exc.message
- end
- assert_not_nil ImmutableDeveloper.find_by_id(1)
-
- someone = CallbackCancellationDeveloper.find(1)
- someone.cancel_before_destroy = true
- assert_deprecated do
- assert !someone.destroy
- assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
- end
- assert !someone.after_destroy_called
- end
-
def assert_save_callbacks_not_called(someone)
assert !someone.after_save_called
assert !someone.after_create_called
@@ -527,7 +402,7 @@ class CallbacksTest < ActiveRecord::TestCase
assert david.valid?
assert !david.save
exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
- assert_equal exc.record, david
+ assert_equal david, exc.record
david = DeveloperWithCanceledCallbacks.find(1)
david.salary = 10_000_000
@@ -554,7 +429,7 @@ class CallbacksTest < ActiveRecord::TestCase
david = DeveloperWithCanceledCallbacks.find(1)
assert !david.destroy
exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
- assert_equal exc.record, david
+ assert_equal david, exc.record
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackHaltedDeveloper.find(1)
@@ -564,50 +439,19 @@ class CallbacksTest < ActiveRecord::TestCase
assert !someone.after_destroy_called
end
- def test_callback_returning_false
- david = CallbackDeveloperWithFalseValidation.find(1)
- assert_deprecated { david.save }
- assert_equal [
- [ :after_find, :method ],
- [ :after_find, :string ],
- [ :after_find, :proc ],
- [ :after_find, :object ],
- [ :after_find, :block ],
- [ :after_initialize, :method ],
- [ :after_initialize, :string ],
- [ :after_initialize, :proc ],
- [ :after_initialize, :object ],
- [ :after_initialize, :block ],
- [ :before_validation, :method ],
- [ :before_validation, :string ],
- [ :before_validation, :proc ],
- [ :before_validation, :object ],
- [ :before_validation, :block ],
- [ :before_validation, :returning_false ],
- [ :after_rollback, :block ],
- [ :after_rollback, :object ],
- [ :after_rollback, :proc ],
- [ :after_rollback, :string ],
- [ :after_rollback, :method ],
- ], david.history
- end
-
def test_callback_throwing_abort
david = CallbackDeveloperWithHaltedValidation.find(1)
david.save
assert_equal [
[ :after_find, :method ],
- [ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
- [ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
- [ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
@@ -615,7 +459,6 @@ class CallbacksTest < ActiveRecord::TestCase
[ :after_rollback, :block ],
[ :after_rollback, :object ],
[ :after_rollback, :proc ],
- [ :after_rollback, :string ],
[ :after_rollback, :method ],
], david.history
end
diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb
index b9c6224425..59ef389326 100644
--- a/activerecord/test/cases/coders/yaml_column_test.rb
+++ b/activerecord/test/cases/coders/yaml_column_test.rb
@@ -5,46 +5,48 @@ module ActiveRecord
module Coders
class YAMLColumnTest < ActiveRecord::TestCase
def test_initialize_takes_class
- coder = YAMLColumn.new(Object)
+ coder = YAMLColumn.new("attr_name", Object)
assert_equal Object, coder.object_class
end
def test_type_mismatch_on_different_classes_on_dump
- coder = YAMLColumn.new(Array)
- assert_raises(SerializationTypeMismatch) do
+ coder = YAMLColumn.new("tags", Array)
+ error = assert_raises(SerializationTypeMismatch) do
coder.dump("a")
end
+ assert_equal %{can't dump `tags`: was supposed to be a Array, but was a String. -- "a"}, error.to_s
end
def test_type_mismatch_on_different_classes
- coder = YAMLColumn.new(Array)
- assert_raises(SerializationTypeMismatch) do
+ coder = YAMLColumn.new("tags", Array)
+ error = assert_raises(SerializationTypeMismatch) do
coder.load "--- foo"
end
+ assert_equal %{can't load `tags`: was supposed to be a Array, but was a String. -- "foo"}, error.to_s
end
def test_nil_is_ok
- coder = YAMLColumn.new
+ coder = YAMLColumn.new("attr_name")
assert_nil coder.load "--- "
end
def test_returns_new_with_different_class
- coder = YAMLColumn.new SerializationTypeMismatch
+ coder = YAMLColumn.new("attr_name", SerializationTypeMismatch)
assert_equal SerializationTypeMismatch, coder.load("--- ").class
end
def test_returns_string_unless_starts_with_dash
- coder = YAMLColumn.new
+ coder = YAMLColumn.new("attr_name")
assert_equal "foo", coder.load("foo")
end
def test_load_handles_other_classes
- coder = YAMLColumn.new
+ coder = YAMLColumn.new("attr_name")
assert_equal [], coder.load([])
end
def test_load_doesnt_swallow_yaml_exceptions
- coder = YAMLColumn.new
+ coder = YAMLColumn.new("attr_name")
bad_yaml = "--- {"
assert_raises(Psych::SyntaxError) do
coder.load(bad_yaml)
@@ -52,7 +54,7 @@ module ActiveRecord
end
def test_load_doesnt_handle_undefined_class_or_module
- coder = YAMLColumn.new
+ coder = YAMLColumn.new("attr_name")
missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n'
assert_raises(ArgumentError) do
coder.load(missing_class_yaml)
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index a65bb89052..d230700119 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -14,71 +14,19 @@ module ActiveRecord
# Avoid column definitions in create table statements like:
# `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
- column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20))
- column_def = ColumnDefinition.new(
- column.name, "string",
- column.limit, column.precision, column.scale, column.default, column.null)
+ column_def = ColumnDefinition.new("title", "string", limit: 20)
assert_equal "title varchar(20)", @viz.accept(column_def)
end
def test_should_include_default_clause_when_default_is_present
- column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20))
- column_def = ColumnDefinition.new(
- column.name, "string",
- column.limit, column.precision, column.scale, column.default, column.null)
+ column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello")
assert_equal "title varchar(20) DEFAULT 'Hello'", @viz.accept(column_def)
end
def test_should_specify_not_null_if_null_option_is_false
- type_metadata = SqlTypeMetadata.new(limit: 20)
- column = Column.new("title", "Hello", type_metadata, false)
- column_def = ColumnDefinition.new(
- column.name, "string",
- column.limit, column.precision, column.scale, column.default, column.null)
+ column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello", null: false)
assert_equal "title varchar(20) DEFAULT 'Hello' NOT NULL", @viz.accept(column_def)
end
-
- if current_adapter?(:Mysql2Adapter)
- def test_should_set_default_for_mysql_binary_data_types
- type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
- binary_column = MySQL::Column.new("title", "a", type)
- assert_equal "a", binary_column.default
-
- type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
- varbinary_column = MySQL::Column.new("title", "a", type)
- assert_equal "a", varbinary_column.default
- end
-
- def test_should_be_empty_string_default_for_mysql_binary_data_types
- type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
- binary_column = MySQL::Column.new("title", "", type, false)
- assert_equal "", binary_column.default
-
- type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
- varbinary_column = MySQL::Column.new("title", "", type, false)
- assert_equal "", varbinary_column.default
- end
-
- def test_should_not_set_default_for_blob_and_text_data_types
- text_type = MySQL::TypeMetadata.new(SqlTypeMetadata.new(type: :text))
-
- text_column = MySQL::Column.new("title", nil, text_type)
- assert_nil text_column.default
-
- not_null_text_column = MySQL::Column.new("title", nil, text_type, false)
- assert_nil not_null_text_column.default
- end
-
- def test_has_default_should_return_false_for_blob_and_text_data_types
- binary_type = SqlTypeMetadata.new(sql_type: "blob")
- blob_column = MySQL::Column.new("title", nil, binary_type)
- assert !blob_column.has_default?
-
- text_type = SqlTypeMetadata.new(type: :text)
- text_column = MySQL::Column.new("title", nil, text_type)
- assert !text_column.has_default?
- end
- end
end
end
end
diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb
index 262ad319be..63f67a9a16 100644
--- a/activerecord/test/cases/comment_test.rb
+++ b/activerecord/test/cases/comment_test.rb
@@ -2,7 +2,6 @@ require "cases/helper"
require "support/schema_dumping_helper"
if ActiveRecord::Base.connection.supports_comments?
-
class CommentTest < ActiveRecord::TestCase
include SchemaDumpingHelper
@@ -102,6 +101,7 @@ if ActiveRecord::Base.connection.supports_comments?
# Do all the stuff from other tests
@connection.add_column :commenteds, :rating, :integer, comment: "I am running out of imagination"
@connection.change_column :commenteds, :content, :string, comment: "Whoa, content describes itself!"
+ @connection.change_column :commenteds, :content, :string
@connection.change_column :commenteds, :obvious, :string, comment: nil
@connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments"
@@ -113,7 +113,7 @@ if ActiveRecord::Base.connection.supports_comments?
assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output
assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output
assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output
- assert_match %r[t\.index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output
+ assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output
end
def test_schema_dump_omits_blank_comments
@@ -135,5 +135,4 @@ if ActiveRecord::Base.connection.supports_comments?
assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output
end
end
-
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 2c33bf22ab..681399c8bb 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -20,6 +20,66 @@ module ActiveRecord
@handler.remove_connection("readonly")
end
+ def test_establish_connection_using_3_levels_config
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" },
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }
+ },
+ "another_env" => {
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/bad-readonly.sqlite3" },
+ "primary" => { "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" }
+ },
+ "common" => { "adapter" => "sqlite3", "database" => "db/common.sqlite3" }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ @handler.establish_connection(:common)
+ @handler.establish_connection(:primary)
+ @handler.establish_connection(:readonly)
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("readonly")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("primary")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("common")
+ assert_equal "db/common.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ 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
+
+ @handler.establish_connection(:development)
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("development")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ end
+
+ def test_establish_connection_using_top_level_key_in_two_level_config
+ config = {
+ "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ @handler.establish_connection(:development_readonly)
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("development_readonly")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ end
+
def test_retrieve_connection
assert @handler.retrieve_connection(@spec_name)
end
@@ -89,6 +149,41 @@ module ActiveRecord
rd.close
end
+ def test_pool_from_any_process_for_uses_most_recent_spec
+ skip unless current_adapter?(:SQLite3Adapter)
+
+ file = Tempfile.new "lol.sqlite3"
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ pid = fork do
+ ActiveRecord::Base.configurations["arunit"]["database"] = file.path
+ ActiveRecord::Base.establish_connection(:arunit)
+
+ pid2 = fork do
+ wr.write ActiveRecord::Base.connection_config[:database]
+ wr.close
+ end
+
+ Process.waitpid pid2
+ end
+
+ Process.waitpid pid
+
+ wr.close
+
+ assert_equal file.path, rd.read
+
+ rd.close
+ ensure
+ if file
+ file.close
+ file.unlink
+ end
+ end
+
def test_a_class_using_custom_pool_and_switching_back_to_primary
klass2 = Class.new(Base) { def self.name; "klass2"; end }
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index a8955152c3..8faa67255d 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -142,13 +142,13 @@ module ActiveRecord
"database" => "foo",
"host" => "localhost" }
assert_equal expected, actual["default_env"]
- assert_equal nil, actual["production"]
- assert_equal nil, actual["development"]
- assert_equal nil, actual["test"]
- assert_equal nil, actual[:default_env]
- assert_equal nil, actual[:production]
- assert_equal nil, actual[:development]
- assert_equal nil, actual[:test]
+ assert_nil actual["production"]
+ assert_nil actual["development"]
+ assert_nil actual["test"]
+ assert_nil actual[:default_env]
+ assert_nil actual[:production]
+ assert_nil actual[:development]
+ assert_nil actual[:test]
end
def test_blank_with_database_url_with_rails_env
@@ -162,15 +162,15 @@ module ActiveRecord
"host" => "localhost" }
assert_equal expected, actual["not_production"]
- assert_equal nil, actual["production"]
- assert_equal nil, actual["default_env"]
- assert_equal nil, actual["development"]
- assert_equal nil, actual["test"]
- assert_equal nil, actual[:default_env]
- assert_equal nil, actual[:not_production]
- assert_equal nil, actual[:production]
- assert_equal nil, actual[:development]
- assert_equal nil, actual[:test]
+ assert_nil actual["production"]
+ assert_nil actual["default_env"]
+ assert_nil actual["development"]
+ assert_nil actual["test"]
+ assert_nil actual[:default_env]
+ assert_nil actual[:not_production]
+ assert_nil actual[:production]
+ assert_nil actual[:development]
+ assert_nil actual[:test]
end
def test_blank_with_database_url_with_rack_env
@@ -184,15 +184,15 @@ module ActiveRecord
"host" => "localhost" }
assert_equal expected, actual["not_production"]
- assert_equal nil, actual["production"]
- assert_equal nil, actual["default_env"]
- assert_equal nil, actual["development"]
- assert_equal nil, actual["test"]
- assert_equal nil, actual[:default_env]
- assert_equal nil, actual[:not_production]
- assert_equal nil, actual[:production]
- assert_equal nil, actual[:development]
- assert_equal nil, actual[:test]
+ assert_nil actual["production"]
+ assert_nil actual["default_env"]
+ assert_nil actual["development"]
+ assert_nil actual["test"]
+ assert_nil actual[:default_env]
+ assert_nil actual[:not_production]
+ assert_nil actual[:production]
+ assert_nil actual[:development]
+ assert_nil actual[:test]
end
def test_database_url_with_ipv6_host_and_port
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index d4459603af..106323ccc9 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -12,6 +12,33 @@ module ActiveRecord
assert_equal "id", @cache.primary_keys("posts")
end
+ def test_yaml_dump_and_load
+ @cache.columns("posts")
+ @cache.columns_hash("posts")
+ @cache.data_sources("posts")
+ @cache.primary_keys("posts")
+
+ 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 new_cache.data_sources("posts")
+ assert_equal "id", new_cache.primary_keys("posts")
+ end
+ end
+
+ def test_yaml_loads_5_1_dump
+ body = File.open(schema_dump_path).read
+ cache = YAML.load(body)
+
+ assert_no_queries do
+ assert_equal 11, cache.columns("posts").size
+ assert_equal 11, cache.columns_hash("posts").size
+ assert cache.data_sources("posts")
+ assert_equal "id", cache.primary_keys("posts")
+ end
+ end
+
def test_primary_key_for_non_existent_table
assert_nil @cache.primary_keys("omgponies")
end
@@ -45,17 +72,28 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 11, @cache.columns("posts").size
- assert_equal 11, @cache.columns_hash("posts").size
- assert @cache.data_sources("posts")
- assert_equal "id", @cache.primary_keys("posts")
+ assert_no_queries do
+ assert_equal 11, @cache.columns("posts").size
+ assert_equal 11, @cache.columns_hash("posts").size
+ assert @cache.data_sources("posts")
+ assert_equal "id", @cache.primary_keys("posts")
+ end
end
- def test_table_methods_deprecation
- assert_deprecated { assert @cache.table_exists?("posts") }
- assert_deprecated { assert @cache.tables("posts") }
- assert_deprecated { @cache.clear_table_cache!("posts") }
+ def test_data_source_exist
+ assert @cache.data_source_exists?("posts")
+ assert_not @cache.data_source_exists?("foo")
end
+
+ def test_clear_data_source_cache
+ @cache.clear_data_source_cache!("posts")
+ end
+
+ private
+
+ def schema_dump_path
+ "test/assets/schema_dump_5_1.yml"
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
index e2e5445a4e..a348c2d783 100644
--- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -89,12 +89,20 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin
end
def test_decimal_without_scale
- types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)}
- types.each do |type|
- cast_type = @connection.type_map.lookup(type)
-
- assert_equal :decimal, cast_type.type
- assert_equal 2, cast_type.cast(2.1)
+ if current_adapter?(:OracleAdapter)
+ {
+ decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0)},
+ integer: %w{number(2) number(2,0)}
+ }
+ else
+ { decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} }
+ end.each do |expected_type, types|
+ types.each do |type|
+ cast_type = @connection.type_map.lookup(type)
+
+ assert_equal expected_type, cast_type.type
+ assert_equal 2, cast_type.cast(2.1)
+ end
end
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index b08e4f603c..7e88c9cf7a 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -307,14 +307,17 @@ module ActiveRecord
end
end
- def test_automatic_reconnect=
+ def test_automatic_reconnect_restores_after_disconnect
pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
assert pool.automatic_reconnect
assert pool.connection
pool.disconnect!
assert pool.connection
+ end
+ def test_automatic_reconnect_can_be_disabled
+ pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
pool.disconnect!
pool.automatic_reconnect = false
@@ -341,14 +344,18 @@ module ActiveRecord
end
end
+ class ConnectionTestModel < ActiveRecord::Base
+ end
+
def test_connection_notification_is_called
payloads = []
subscription = ActiveSupport::Notifications.subscribe("!connection.active_record") do |name, started, finished, unique_id, payload|
payloads << payload
end
- ActiveRecord::Base.establish_connection :arunit
+ ConnectionTestModel.establish_connection :arunit
+
assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort
- assert_equal "primary", payloads[0][:spec_name]
+ assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name]
ensure
ActiveSupport::Notifications.unsubscribe(subscription) if subscription
end
@@ -395,7 +402,7 @@ module ActiveRecord
all_threads_in_new_connection.wait
end
rescue Timeout::Error
- flunk "pool unable to establish connections concurrently or implementation has " <<
+ flunk "pool unable to establish connections concurrently or implementation has " \
"changed, this test then needs to patch a different :new_connection method"
ensure
# clean up the threads
@@ -437,7 +444,7 @@ module ActiveRecord
end
end
- def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway
+ def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_all_connections_proceed_anyway
@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|
@@ -455,49 +462,63 @@ module ActiveRecord
with_single_connection_pool do |pool|
[:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
conn = pool.connection # drain the only available connection
- second_thread_done = Concurrent::CountDownLatch.new
+ second_thread_done = Concurrent::Event.new
- # create a first_thread and let it get into the FIFO queue first
- first_thread = Thread.new do
- pool.with_connection { second_thread_done.wait }
- end
-
- # wait for first_thread to get in queue
- Thread.pass until pool.num_waiting_in_queue == 1
-
- # create a different, later thread, that will attempt to do a "group action",
- # but because of the group action semantics it should be able to preempt the
- # first_thread when a connection is made available
- second_thread = Thread.new do
- pool.send(group_action_method)
- second_thread_done.count_down
- end
-
- # wait for second_thread to get in queue
- Thread.pass until pool.num_waiting_in_queue == 2
-
- # return the only available connection
- pool.checkin(conn)
-
- # if the second_thread is not able to preempt the first_thread,
- # they will temporarily (until either of them timeouts with ConnectionTimeoutError)
- # deadlock and a join(2) timeout will be reached
- failed = true unless second_thread.join(2)
-
- #--- post test clean up start
- second_thread_done.count_down if failed
-
- # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for
- # it to timeout with ConnectionTimeoutError
- if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0
- pool.with_connection {} # create a new connection in case there are threads still stuck in a queue
+ begin
+ # create a first_thread and let it get into the FIFO queue first
+ first_thread = Thread.new do
+ pool.with_connection { second_thread_done.wait }
+ end
+
+ # wait for first_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 1
+
+ # create a different, later thread, that will attempt to do a "group action",
+ # but because of the group action semantics it should be able to preempt the
+ # first_thread when a connection is made available
+ second_thread = Thread.new do
+ pool.send(group_action_method)
+ second_thread_done.set
+ end
+
+ # wait for second_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 2
+
+ # return the only available connection
+ pool.checkin(conn)
+
+ # if the second_thread is not able to preempt the first_thread,
+ # they will temporarily (until either of them timeouts with ConnectionTimeoutError)
+ # deadlock and a join(2) timeout will be reached
+ assert second_thread.join(2), "#{group_action_method} is not able to preempt other waiting threads"
+
+ ensure
+ # post test clean up
+ failed = !second_thread_done.set?
+
+ if failed
+ second_thread_done.set
+
+ puts
+ puts ">>> test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads / #{group_action_method}"
+ p [first_thread, second_thread]
+ p pool.stat
+ p pool.connections.map(&:owner)
+
+ first_thread.join(2)
+ second_thread.join(2)
+
+ puts "---"
+ p [first_thread, second_thread]
+ p pool.stat
+ p pool.connections.map(&:owner)
+ puts "<<<"
+ puts
+ end
+
+ first_thread.join(10) || raise("first_thread got stuck")
+ second_thread.join(10) || raise("second_thread got stuck")
end
-
- first_thread.join
- second_thread.join
- #--- post test clean up end
-
- flunk "#{group_action_method} is not able to preempt other waiting threads" if failed
end
end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 84f2c3a465..46d7526cc0 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -211,4 +211,155 @@ class CounterCacheTest < ActiveRecord::TestCase
aircraft.wheels.first.destroy
end
end
+
+ test "update counters doesn't touch timestamps by default" do
+ @topic.update_column :updated_at, 5.minutes.ago
+ previously_updated_at = @topic.updated_at
+
+ Topic.update_counters(@topic.id, replies_count: -1)
+
+ assert_equal previously_updated_at, @topic.updated_at
+ end
+
+ test "update counters doesn't touch timestamps with touch: []" do
+ @topic.update_column :updated_at, 5.minutes.ago
+ previously_updated_at = @topic.updated_at
+
+ Topic.update_counters(@topic.id, replies_count: -1, touch: [])
+
+ assert_equal previously_updated_at, @topic.updated_at
+ end
+
+ test "update counters with touch: true" do
+ assert_touching @topic, :updated_at do
+ Topic.update_counters(@topic.id, replies_count: -1, touch: true)
+ end
+ end
+
+ test "update counters of multiple records with touch: true" do
+ t1, t2 = topics(:first, :second)
+
+ assert_touching t1, :updated_at do
+ assert_difference ["t1.reload.replies_count", "t2.reload.replies_count"], 2 do
+ Topic.update_counters([t1.id, t2.id], replies_count: 2, touch: true)
+ end
+ end
+ end
+
+ test "update multiple counters with touch: true" do
+ assert_touching @topic, :updated_at do
+ Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: true)
+ end
+ end
+
+ test "reset counters with touch: true" do
+ assert_touching @topic, :updated_at do
+ Topic.reset_counters(@topic.id, :replies, touch: true)
+ end
+ end
+
+ test "reset multiple counters with touch: true" do
+ assert_touching @topic, :updated_at do
+ Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1)
+ Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: true)
+ end
+ end
+
+ test "increment counters with touch: true" do
+ assert_touching @topic, :updated_at do
+ Topic.increment_counter(:replies_count, @topic.id, touch: true)
+ end
+ end
+
+ test "decrement counters with touch: true" do
+ assert_touching @topic, :updated_at do
+ Topic.decrement_counter(:replies_count, @topic.id, touch: true)
+ end
+ end
+
+ test "update counters with touch: :written_on" do
+ assert_touching @topic, :written_on do
+ Topic.update_counters(@topic.id, replies_count: -1, touch: :written_on)
+ end
+ end
+
+ test "update multiple counters with touch: :written_on" do
+ assert_touching @topic, :written_on do
+ Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: :written_on)
+ end
+ end
+
+ test "reset counters with touch: :written_on" do
+ assert_touching @topic, :written_on do
+ Topic.reset_counters(@topic.id, :replies, touch: :written_on)
+ end
+ end
+
+ test "reset multiple counters with touch: :written_on" do
+ assert_touching @topic, :written_on do
+ Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1)
+ Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: :written_on)
+ end
+ end
+
+ test "increment counters with touch: :written_on" do
+ assert_touching @topic, :written_on do
+ Topic.increment_counter(:replies_count, @topic.id, touch: :written_on)
+ end
+ end
+
+ test "decrement counters with touch: :written_on" do
+ assert_touching @topic, :written_on do
+ Topic.decrement_counter(:replies_count, @topic.id, touch: :written_on)
+ end
+ end
+
+ test "update counters with touch: %i( updated_at written_on )" do
+ assert_touching @topic, :updated_at, :written_on do
+ Topic.update_counters(@topic.id, replies_count: -1, touch: %i( updated_at written_on ))
+ end
+ end
+
+ test "update multiple counters with touch: %i( updated_at written_on )" do
+ assert_touching @topic, :updated_at, :written_on do
+ Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: %i( updated_at written_on ))
+ end
+ end
+
+ test "reset counters with touch: %i( updated_at written_on )" do
+ assert_touching @topic, :updated_at, :written_on do
+ Topic.reset_counters(@topic.id, :replies, touch: %i( updated_at written_on ))
+ end
+ end
+
+ test "reset multiple counters with touch: %i( updated_at written_on )" do
+ assert_touching @topic, :updated_at, :written_on do
+ Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1)
+ Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: %i( updated_at written_on ))
+ end
+ end
+
+ test "increment counters with touch: %i( updated_at written_on )" do
+ assert_touching @topic, :updated_at, :written_on do
+ Topic.increment_counter(:replies_count, @topic.id, touch: %i( updated_at written_on ))
+ end
+ end
+
+ test "decrement counters with touch: %i( updated_at written_on )" do
+ assert_touching @topic, :updated_at, :written_on do
+ Topic.decrement_counter(:replies_count, @topic.id, touch: %i( updated_at written_on ))
+ end
+ end
+
+ private
+ def assert_touching(record, *attributes)
+ record.update_columns attributes.map { |attr| [ attr, 5.minutes.ago ] }.to_h
+ touch_times = attributes.map { |attr| [ attr, record.public_send(attr) ] }.to_h
+
+ yield
+
+ touch_times.each do |attr, previous_touch_time|
+ assert_operator previous_touch_time, :<, record.reload.public_send(attr)
+ end
+ end
end
diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb
index bb16076fd2..66035865be 100644
--- a/activerecord/test/cases/database_statements_test.rb
+++ b/activerecord/test/cases/database_statements_test.rb
@@ -20,12 +20,6 @@ class DatabaseStatementsTest < ActiveRecord::TestCase
assert_not_nil return_the_inserted_id(method: :create)
end
- def test_insert_update_delete_sql_is_deprecated
- assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") }
- assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") }
- assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") }
- end
-
private
def return_the_inserted_id(method:)
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index a1c3c5af9c..e4a2f9ee17 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -73,7 +73,7 @@ if subsecond_precision_supported?
assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output
end
- if current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter)
def test_datetime_precision_with_zero_should_be_dumped
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 0
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 3bc08f80ec..ad7da9de70 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -52,7 +52,7 @@ class DateTimeTest < ActiveRecord::TestCase
end
def test_assign_in_local_timezone
- now = DateTime.now
+ now = DateTime.civil(2017, 3, 1, 12, 0, 0)
with_timezone_config default: :local do
task = Task.new starting: now
assert_equal now, task.starting
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index fcaff38f82..a6297673c9 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -100,11 +100,21 @@ if current_adapter?(:Mysql2Adapter)
include SchemaDumpingHelper
if ActiveRecord::Base.connection.version >= "5.6.0"
- test "schema dump includes default expression" do
+ test "schema dump datetime includes default expression" do
output = dump_table_schema("datetime_defaults")
assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
end
end
+
+ test "schema dump timestamp includes default expression" do
+ output = dump_table_schema("timestamp_defaults")
+ assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
+ end
+
+ test "schema dump timestamp without default expression" do
+ output = dump_table_schema("timestamp_defaults")
+ assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output
+ end
end
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
@@ -169,7 +179,7 @@ if current_adapter?(:Mysql2Adapter)
assert_nil record.non_null_text
assert_nil record.non_null_blob
- assert_raises(ActiveRecord::StatementInvalid) { klass.create }
+ assert_raises(ActiveRecord::NotNullViolation) { klass.create }
end
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 0e58e65a07..f9eccfbda1 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -301,6 +301,14 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["arr", "arr matey!"], pirate.catchphrase_change
end
+ def test_virtual_attribute_will_change
+ assert_deprecated do
+ parrot = Parrot.create!(name: "Ruby")
+ parrot.send(:attribute_will_change!, :cancel_save_from_callback)
+ assert parrot.has_changes_to_save?
+ end
+ end
+
def test_association_assignment_changes_foreign_key
pirate = Pirate.create!(catchphrase: "jarl")
pirate.parrot = Parrot.create!(name: "Lorre")
@@ -341,13 +349,14 @@ class DirtyTest < ActiveRecord::TestCase
def test_partial_update_with_optimistic_locking
person = Person.new(first_name: "foo")
- old_lock_version = person.lock_version
with_partial_writes Person, false do
assert_queries(2) { 2.times { person.save! } }
Person.where(id: person.id).update_all(first_name: "baz")
end
+ old_lock_version = person.lock_version
+
with_partial_writes Person, true do
assert_queries(0) { 2.times { person.save! } }
assert_equal old_lock_version, person.reload.lock_version
@@ -558,18 +567,17 @@ class DirtyTest < ActiveRecord::TestCase
travel_back
end
- if ActiveRecord::Base.connection.supports_migrations?
- class Testings < ActiveRecord::Base; end
- def test_field_named_field
- ActiveRecord::Base.connection.create_table :testings do |t|
- t.string :field
- end
- assert_nothing_raised do
- Testings.new.attributes
- end
- ensure
- ActiveRecord::Base.connection.drop_table :testings rescue nil
+ class Testings < ActiveRecord::Base; end
+ def test_field_named_field
+ ActiveRecord::Base.connection.create_table :testings do |t|
+ t.string :field
end
+ assert_nothing_raised do
+ Testings.new.attributes
+ end
+ ensure
+ ActiveRecord::Base.connection.drop_table :testings rescue nil
+ ActiveRecord::Base.clear_cache!
end
def test_datetime_attribute_can_be_updated_with_fractional_seconds
diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb
index 0711a372f2..73feb831d0 100644
--- a/activerecord/test/cases/errors_test.rb
+++ b/activerecord/test/cases/errors_test.rb
@@ -5,7 +5,7 @@ class ErrorsTest < ActiveRecord::TestCase
base = ActiveRecord::ActiveRecordError
error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base }
- error_klasses.each do |error_klass|
+ (error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass|
begin
error_klass.new.inspect
rescue ArgumentError
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 7eaf31aa24..89d8a8bdca 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -117,8 +117,8 @@ class FinderTest < ActiveRecord::TestCase
assert_equal "The Fourth Topic of the day", records[2].title
end
- def test_find_passing_active_record_object_is_deprecated
- assert_deprecated do
+ def test_find_passing_active_record_object_is_not_permitted
+ assert_raises(ArgumentError) do
Topic.find(Topic.last)
end
end
@@ -167,8 +167,8 @@ class FinderTest < ActiveRecord::TestCase
assert_equal false, relation.exists?(false)
end
- def test_exists_passing_active_record_object_is_deprecated
- assert_deprecated do
+ def test_exists_passing_active_record_object_is_not_permitted
+ assert_raises(ArgumentError) do
Topic.exists?(Topic.new)
end
end
@@ -339,6 +339,11 @@ class FinderTest < ActiveRecord::TestCase
assert_equal author.post, Post.find_by(author_id: Author.where(id: author))
end
+ def test_find_by_and_where_consistency_with_active_record_instance
+ author = authors(:david)
+ assert_equal Post.where(author_id: author).take, Post.find_by(author_id: author)
+ end
+
def test_take
assert_equal topics(:first), Topic.take
end
@@ -488,12 +493,12 @@ class FinderTest < ActiveRecord::TestCase
assert_equal topics(:fourth), Topic.offset(1).second_to_last
assert_equal topics(:fourth), Topic.offset(2).second_to_last
assert_equal topics(:fourth), Topic.offset(3).second_to_last
- assert_equal nil, Topic.offset(4).second_to_last
- assert_equal nil, Topic.offset(5).second_to_last
+ assert_nil Topic.offset(4).second_to_last
+ assert_nil Topic.offset(5).second_to_last
#test with limit
- # assert_equal nil, Topic.limit(1).second # TODO: currently failing
- assert_equal nil, Topic.limit(1).second_to_last
+ assert_nil Topic.limit(1).second
+ assert_nil Topic.limit(1).second_to_last
end
def test_second_to_last_have_primary_key_order_by_default
@@ -516,15 +521,15 @@ class FinderTest < ActiveRecord::TestCase
# test with offset
assert_equal topics(:third), Topic.offset(1).third_to_last
assert_equal topics(:third), Topic.offset(2).third_to_last
- assert_equal nil, Topic.offset(3).third_to_last
- assert_equal nil, Topic.offset(4).third_to_last
- assert_equal nil, Topic.offset(5).third_to_last
+ assert_nil Topic.offset(3).third_to_last
+ assert_nil Topic.offset(4).third_to_last
+ assert_nil Topic.offset(5).third_to_last
# test with limit
- # assert_equal nil, Topic.limit(1).third # TODO: currently failing
- assert_equal nil, Topic.limit(1).third_to_last
- # assert_equal nil, Topic.limit(2).third # TODO: currently failing
- assert_equal nil, Topic.limit(2).third_to_last
+ assert_nil Topic.limit(1).third
+ assert_nil Topic.limit(1).third_to_last
+ assert_nil Topic.limit(2).third
+ assert_nil Topic.limit(2).third_to_last
end
def test_third_to_last_have_primary_key_order_by_default
@@ -592,7 +597,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_last_with_irreversible_order
- assert_deprecated do
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
Topic.order("coalesce(author_name, title)").last
end
end
@@ -858,13 +863,13 @@ class FinderTest < ActiveRecord::TestCase
end
def test_bind_variables_with_quotes
- Company.create("name" => "37signals' go'es agains")
- assert Company.where(["name = ?", "37signals' go'es agains"]).first
+ Company.create("name" => "37signals' go'es against")
+ assert Company.where(["name = ?", "37signals' go'es against"]).first
end
def test_named_bind_variables_with_quotes
- Company.create("name" => "37signals' go'es agains")
- assert Company.where(["name = :name", { name: "37signals' go'es agains" }]).first
+ Company.create("name" => "37signals' go'es against")
+ assert Company.where(["name = :name", { name: "37signals' go'es against" }]).first
end
def test_named_bind_variables
@@ -983,7 +988,6 @@ class FinderTest < ActiveRecord::TestCase
assert_equal devs[2], Developer.offset(2).first
assert_equal devs[-3], Developer.offset(2).last
- assert_equal devs[-3], Developer.offset(2).last
assert_equal devs[-3], Developer.offset(2).order("id DESC").first
end
@@ -1167,7 +1171,7 @@ class FinderTest < ActiveRecord::TestCase
end
test "find_by returns nil if the record is missing" do
- assert_equal nil, Post.find_by("1 = 0")
+ assert_nil Post.find_by("1 = 0")
end
test "find_by with associations" do
@@ -1221,7 +1225,7 @@ class FinderTest < ActiveRecord::TestCase
assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id)
end
- protected
+ private
def table_with_custom_primary_key
yield(Class.new(Toy) do
def self.name
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index f3d0e4a1b1..3720b0cc1a 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -7,17 +7,19 @@ require "models/binary"
require "models/book"
require "models/bulb"
require "models/category"
+require "models/post"
require "models/comment"
require "models/company"
require "models/computer"
require "models/course"
require "models/developer"
+require "models/dog"
require "models/doubloon"
require "models/joke"
require "models/matey"
+require "models/other_dog"
require "models/parrot"
require "models/pirate"
-require "models/post"
require "models/randomly_named_c1"
require "models/reply"
require "models/ship"
@@ -102,64 +104,62 @@ class FixturesTest < ActiveRecord::TestCase
assert_nil(second_row["author_email_address"])
end
- if ActiveRecord::Base.connection.supports_migrations?
- def test_inserts_with_pre_and_suffix
- # Reset cache to make finds on the new table work
- ActiveRecord::FixtureSet.reset_cache
-
- ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t|
- t.column :title, :string
- t.column :author_name, :string
- t.column :author_email_address, :string
- t.column :written_on, :datetime
- t.column :bonus_time, :time
- t.column :last_read, :date
- t.column :content, :string
- t.column :approved, :boolean, default: true
- t.column :replies_count, :integer, default: 0
- t.column :parent_id, :integer
- t.column :type, :string, limit: 50
- end
+ def test_inserts_with_pre_and_suffix
+ # Reset cache to make finds on the new table work
+ ActiveRecord::FixtureSet.reset_cache
- # Store existing prefix/suffix
- old_prefix = ActiveRecord::Base.table_name_prefix
- old_suffix = ActiveRecord::Base.table_name_suffix
+ ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t|
+ t.column :title, :string
+ t.column :author_name, :string
+ t.column :author_email_address, :string
+ t.column :written_on, :datetime
+ t.column :bonus_time, :time
+ t.column :last_read, :date
+ t.column :content, :string
+ t.column :approved, :boolean, default: true
+ t.column :replies_count, :integer, default: 0
+ t.column :parent_id, :integer
+ t.column :type, :string, limit: 50
+ end
- # Set a prefix/suffix we can test against
- ActiveRecord::Base.table_name_prefix = "prefix_"
- ActiveRecord::Base.table_name_suffix = "_suffix"
+ # Store existing prefix/suffix
+ old_prefix = ActiveRecord::Base.table_name_prefix
+ old_suffix = ActiveRecord::Base.table_name_suffix
- other_topic_klass = Class.new(ActiveRecord::Base) do
- def self.name
- "OtherTopic"
- end
+ # Set a prefix/suffix we can test against
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+
+ other_topic_klass = Class.new(ActiveRecord::Base) do
+ def self.name
+ "OtherTopic"
end
+ end
- topics = [create_fixtures("other_topics")].flatten.first
+ topics = [create_fixtures("other_topics")].flatten.first
- # This checks for a caching problem which causes a bug in the fixtures
- # class-level configuration helper.
- assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create"
+ # This checks for a caching problem which causes a bug in the fixtures
+ # class-level configuration helper.
+ assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create"
- first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'")
- assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found"
- assert_equal("The First Topic", first_row["title"])
+ first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'")
+ assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found"
+ assert_equal("The First Topic", first_row["title"])
- second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'")
- assert_nil(second_row["author_email_address"])
+ second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'")
+ assert_nil(second_row["author_email_address"])
- assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym
- # This assertion should preferably be the last in the list, because calling
- # other_topic_klass.table_name sets a class-level instance variable
- assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym
+ assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym
+ # This assertion should preferably be the last in the list, because calling
+ # other_topic_klass.table_name sets a class-level instance variable
+ assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym
- ensure
- # Restore prefix/suffix to its previous values
- ActiveRecord::Base.table_name_prefix = old_prefix
- ActiveRecord::Base.table_name_suffix = old_suffix
+ ensure
+ # Restore prefix/suffix to its previous values
+ ActiveRecord::Base.table_name_prefix = old_prefix
+ ActiveRecord::Base.table_name_suffix = old_suffix
- ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil
- end
+ ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil
end
def test_insert_with_datetime
@@ -211,9 +211,19 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_dirty_dirty_yaml_file
- assert_raise(ActiveRecord::Fixture::FormatError) do
- ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses")
+ fixture_path = FIXTURES_ROOT + "/naked/yml/courses"
+ error = assert_raise(ActiveRecord::Fixture::FormatError) do
+ ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path)
+ end
+ assert_equal "fixture is not a hash: #{fixture_path}.yml", error.to_s
+ end
+
+ def test_yaml_file_with_one_invalid_fixture
+ fixture_path = FIXTURES_ROOT + "/naked/yml/courses_with_invalid_key"
+ error = assert_raise(ActiveRecord::Fixture::FormatError) do
+ ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path)
end
+ assert_equal "fixture key is not a hash: #{fixture_path}.yml, keys: [\"two\"]", error.to_s
end
def test_yaml_file_with_invalid_column
@@ -628,6 +638,8 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase
def test_transaction_created_on_connection_notification
connection = stub(transaction_open?: false)
connection.expects(:begin_transaction).with(joinable: false)
+ pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec))
+ pool.stubs(:lock_thread=).with(false)
fire_connection_notification(connection)
end
@@ -635,12 +647,16 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase
# Mocha is not thread-safe so define our own stub to test
connection = Class.new do
attr_accessor :rollback_transaction_called
+ attr_accessor :pool
def transaction_open?; true; end
def begin_transaction(*args); end
def rollback_transaction(*args)
@rollback_transaction_called = true
end
end.new
+ connection.pool = Class.new do
+ def lock_thread=(lock_thread); false; end
+ end.new
fire_connection_notification(connection)
teardown_fixtures
assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not")
@@ -750,7 +766,7 @@ end
class FasterFixturesTest < ActiveRecord::TestCase
self.use_transactional_tests = false
- fixtures :categories, :authors
+ fixtures :categories, :authors, :author_addresses
def load_extra_fixture(name)
fixture = create_fixtures(name).first
@@ -1000,7 +1016,7 @@ end
class FixtureClassNamesTest < ActiveRecord::TestCase
def setup
- @saved_cache = self.fixture_class_names.dup
+ @saved_cache = fixture_class_names.dup
end
def teardown
@@ -1011,3 +1027,16 @@ class FixtureClassNamesTest < ActiveRecord::TestCase
assert_nil fixture_class_names["unregistered_identifier"]
end
end
+
+class SameNameDifferentDatabaseFixturesTest < ActiveRecord::TestCase
+ fixtures :dogs, :other_dogs
+
+ test "fixtures are properly loaded" do
+ # Force loading the fixtures again to reproduce issue
+ ActiveRecord::FixtureSet.reset_cache
+ create_fixtures("dogs", "other_dogs")
+
+ assert_kind_of Dog, dogs(:sophie)
+ assert_kind_of OtherDog, other_dogs(:lassie)
+ end
+end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index f1d69a215a..5a3b8e3fb5 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -6,7 +6,6 @@ require "active_record"
require "cases/test_case"
require "active_support/dependencies"
require "active_support/logger"
-require "active_support/core_ext/string/strip"
require "support/config"
require "support/connection"
@@ -27,9 +26,6 @@ ARTest.connect
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type")
-# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends.
-ActiveRecord::Base.time_zone_aware_types << :time
-
def current_adapter?(*types)
types.any? do |type|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 9ad4664567..e570e9ac1d 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -58,21 +58,21 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_compute_type_success
- assert_equal Author, ActiveRecord::Base.send(:compute_type, "Author")
+ assert_equal Author, Company.send(:compute_type, "Author")
end
def test_compute_type_nonexistent_constant
e = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, "NonexistentModel"
+ Company.send :compute_type, "NonexistentModel"
end
- assert_equal "uninitialized constant ActiveRecord::Base::NonexistentModel", e.message
- assert_equal "ActiveRecord::Base::NonexistentModel", e.name
+ assert_equal "uninitialized constant Company::NonexistentModel", e.message
+ assert_equal "Company::NonexistentModel", e.name
end
def test_compute_type_no_method_error
ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise NoMethodError }) do
assert_raises NoMethodError do
- ActiveRecord::Base.send :compute_type, "InvalidModel"
+ Company.send :compute_type, "InvalidModel"
end
end
end
@@ -90,7 +90,7 @@ class InheritanceTest < ActiveRecord::TestCase
ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do
exception = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, "InvalidModel"
+ Company.send :compute_type, "InvalidModel"
end
assert_equal error.message, exception.message
end
@@ -99,7 +99,7 @@ class InheritanceTest < ActiveRecord::TestCase
def test_compute_type_argument_error
ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise ArgumentError }) do
assert_raises ArgumentError do
- ActiveRecord::Base.send :compute_type, "InvalidModel"
+ Company.send :compute_type, "InvalidModel"
end
end
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 00457965d7..d7aa091623 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -15,7 +15,7 @@ class IntegrationTest < ActiveRecord::TestCase
def test_to_param_returns_nil_if_not_persisted
client = Client.new
- assert_equal nil, client.to_param
+ assert_nil client.to_param
end
def test_to_param_returns_id_if_not_persisted_but_id_is_set
@@ -89,7 +89,7 @@ class IntegrationTest < ActiveRecord::TestCase
def test_to_param_class_method_uses_default_if_not_persisted
firm = Firm.new(name: "Fancy Shirts")
- assert_equal nil, firm.to_param
+ assert_nil firm.to_param
end
def test_to_param_with_no_arguments
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 9d5aace7db..cc3951e2ba 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -165,10 +165,8 @@ module ActiveRecord
teardown do
%w[horses new_horses].each do |table|
- ActiveSupport::Deprecation.silence do
- if ActiveRecord::Base.connection.table_exists?(table)
- ActiveRecord::Base.connection.drop_table(table)
- end
+ if ActiveRecord::Base.connection.table_exists?(table)
+ ActiveRecord::Base.connection.drop_table(table)
end
end
ActiveRecord::Migration.verbose = @verbose_was
@@ -199,14 +197,14 @@ module ActiveRecord
def test_migrate_up
migration = InvertibleMigration.new
migration.migrate(:up)
- ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" }
+ assert migration.connection.table_exists?("horses"), "horses should exist"
end
def test_migrate_down
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert !migration.connection.table_exists?("horses")
end
def test_migrate_revert
@@ -214,11 +212,11 @@ module ActiveRecord
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert !migration.connection.table_exists?("horses")
revert.migrate :down
- ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") }
+ assert migration.connection.table_exists?("horses")
migration.migrate :down
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert !migration.connection.table_exists?("horses")
end
def test_migrate_revert_by_part
@@ -226,24 +224,18 @@ module ActiveRecord
received = []
migration = InvertibleByPartsMigration.new
migration.test = ->(dir) {
- ActiveSupport::Deprecation.silence do
- assert migration.connection.table_exists?("horses")
- assert migration.connection.table_exists?("new_horses")
- end
+ assert migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
received << dir
}
migration.migrate :up
assert_equal [:both, :up], received
- ActiveSupport::Deprecation.silence do
- assert !migration.connection.table_exists?("horses")
- assert migration.connection.table_exists?("new_horses")
- end
+ assert !migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
- ActiveSupport::Deprecation.silence do
- assert migration.connection.table_exists?("horses")
- assert !migration.connection.table_exists?("new_horses")
- end
+ assert migration.connection.table_exists?("horses")
+ assert !migration.connection.table_exists?("new_horses")
end
def test_migrate_revert_whole_migration
@@ -252,20 +244,20 @@ module ActiveRecord
revert = RevertWholeMigration.new(klass)
migration.migrate :up
revert.migrate :up
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert !migration.connection.table_exists?("horses")
revert.migrate :down
- ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") }
+ assert migration.connection.table_exists?("horses")
migration.migrate :down
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert !migration.connection.table_exists?("horses")
end
end
def test_migrate_nested_revert_whole_migration
revert = NestedRevertWholeMigration.new(InvertibleRevertMigration)
revert.migrate :down
- ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") }
+ assert revert.connection.table_exists?("horses")
revert.migrate :up
- ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") }
+ assert !revert.connection.table_exists?("horses")
end
def test_migrate_revert_change_column_default
@@ -330,24 +322,24 @@ module ActiveRecord
def test_legacy_up
LegacyMigration.migrate :up
- ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" }
+ assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
end
def test_legacy_down
LegacyMigration.migrate :up
LegacyMigration.migrate :down
- ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" }
+ assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_up
LegacyMigration.up
- ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" }
+ assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
end
def test_down
LegacyMigration.up
LegacyMigration.down
- ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" }
+ assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_migrate_down_with_table_name_prefix
@@ -356,7 +348,7 @@ module ActiveRecord
migration = InvertibleMigration.new
migration.migrate(:up)
assert_nothing_raised { migration.migrate(:down) }
- ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" }
+ assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist"
ensure
ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ""
end
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index a2150483f3..9b4b61b16e 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -101,6 +101,17 @@ class JsonSerializationTest < ActiveRecord::TestCase
assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json
end
+ def test_uses_serializable_hash_with_frozen_hash
+ def @contact.serializable_hash(options = nil)
+ super({ only: %w(name) }.freeze)
+ end
+
+ json = @contact.to_json
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_no_match %r{awesome}, json
+ assert_no_match %r{age}, json
+ end
+
def test_uses_serializable_hash_with_only_option
def @contact.serializable_hash(options = nil)
super(only: %w(name))
@@ -157,7 +168,7 @@ class JsonSerializationTest < ActiveRecord::TestCase
end
class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
- fixtures :authors, :posts, :comments, :tags, :taggings
+ fixtures :authors, :author_addresses, :posts, :comments, :tags, :taggings
include JsonSerializationHelpers
@@ -243,7 +254,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
assert !@david.posts.first.respond_to?(:favorite_quote)
assert_match %r{"favorite_quote":"Constraints are liberating"}, json
- assert_equal %r{"favorite_quote":}.match(json).size, 1
+ assert_equal 1, %r{"favorite_quote":}.match(json).size
end
def test_should_allow_only_option_for_list_of_authors
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 9b0ec54aa7..3a3b8e51f9 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -18,7 +18,7 @@ class LockWithoutDefault < ActiveRecord::Base; end
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
self.table_name = :lock_without_defaults_cust
- self.column_defaults # to test @column_defaults caching.
+ column_defaults # to test @column_defaults caching.
self.locking_column = :custom_lock_version
end
@@ -247,17 +247,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal "new title2", t2.title
end
- def test_lock_without_default_should_update_with_lock_col
- t1 = LockWithoutDefault.create(title: "title1", lock_version: 6)
-
- assert_equal 6, t1.lock_version
-
- t1.update(lock_version: 0)
- t1.reload
-
- assert_equal 0, t1.lock_version
- end
-
def test_lock_without_default_queries_count
t1 = LockWithoutDefault.create(title: "title1")
@@ -270,12 +259,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal "title2", t1.title
assert_equal 1, t1.lock_version
- assert_queries(1) { t1.update(title: "title3", lock_version: 6) }
-
- t1.reload
- assert_equal "title3", t1.title
- assert_equal 6, t1.lock_version
-
t2 = LockWithoutDefault.new(title: "title1")
assert_queries(1) { t2.save! }
@@ -321,17 +304,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal "new title2", t2.title
end
- def test_lock_with_custom_column_without_default_should_update_with_lock_col
- t1 = LockWithCustomColumnWithoutDefault.create(title: "title1", custom_lock_version: 6)
-
- assert_equal 6, t1.custom_lock_version
-
- t1.update(custom_lock_version: 0)
- t1.reload
-
- assert_equal 0, t1.custom_lock_version
- end
-
def test_lock_with_custom_column_without_default_queries_count
t1 = LockWithCustomColumnWithoutDefault.create(title: "title1")
@@ -344,12 +316,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal "title2", t1.title
assert_equal 1, t1.custom_lock_version
- assert_queries(1) { t1.update(title: "title3", custom_lock_version: 6) }
-
- t1.reload
- assert_equal "title3", t1.title
- assert_equal 6, t1.custom_lock_version
-
t2 = LockWithCustomColumnWithoutDefault.new(title: "title1")
assert_queries(1) { t2.save! }
@@ -536,7 +502,10 @@ unless in_memory_db?
Person.transaction do
person = Person.find 1
old, person.first_name = person.first_name, "fooman"
- person.lock!
+ # Locking a dirty record is deprecated
+ assert_deprecated do
+ person.lock!
+ end
assert_equal old, person.first_name
end
end
@@ -579,7 +548,7 @@ unless in_memory_db?
assert first.end > second.end
end
- protected
+ private
def duel(zzz = 5)
t0, t1, t2, t3 = nil, nil, nil, nil
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index bdb90eaa74..1d305fa11f 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -43,7 +43,7 @@ module ActiveRecord
t.column :foo, :string, null: false
end
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raises(ActiveRecord::NotNullViolation) do
connection.execute "insert into testings (foo) values (NULL)"
end
end
@@ -233,7 +233,7 @@ module ActiveRecord
end
connection.add_column :testings, :bar, :string, null: false
- assert_raise(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::NotNullViolation) do
connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
end
end
@@ -244,12 +244,16 @@ module ActiveRecord
t.column :foo, :string
end
- con = connection
- connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
- assert_nothing_raised { connection.add_column :testings, :bar, :string, null: false, default: "default" }
+ quoted_id = connection.quote_column_name("id")
+ quoted_foo = connection.quote_column_name("foo")
+ quoted_bar = connection.quote_column_name("bar")
+ connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}) values (1, 'hello')")
+ assert_nothing_raised do
+ connection.add_column :testings, :bar, :string, null: false, default: "default"
+ end
- assert_raises(ActiveRecord::StatementInvalid) do
- connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
+ assert_raises(ActiveRecord::NotNullViolation) do
+ connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}, #{quoted_bar}) values (2, 'hello', NULL)")
end
end
@@ -265,6 +269,8 @@ module ActiveRecord
if current_adapter?(:PostgreSQLAdapter)
assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type
+ elsif current_adapter?(:Mysql2Adapter)
+ assert_equal "timestamp", klass.columns_hash["foo"].sql_type
else
assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type
end
@@ -405,9 +411,9 @@ module ActiveRecord
def test_drop_table_if_exists
connection.create_table(:testings)
- ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) }
+ assert connection.table_exists?(:testings)
connection.drop_table(:testings, if_exists: true)
- ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) }
+ assert_not connection.table_exists?(:testings)
end
def test_drop_table_if_exists_nothing_raised
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 55c06da411..2329888345 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -225,6 +225,16 @@ module ActiveRecord
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?
+
+ change_column "test_models", "contributor", :boolean, default: nil, null: false
+ TestModel.reset_column_information
+ assert_not 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?
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index 0a4b604601..7a80bfb899 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "support/schema_dumping_helper"
module ActiveRecord
class Migration
@@ -55,7 +56,7 @@ module ActiveRecord
end
def test_references_does_not_add_index_by_default
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
def migrate(x)
create_table :more_testings do |t|
t.references :foo
@@ -73,7 +74,7 @@ module ActiveRecord
end
def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
def migrate(x)
create_table :more_testings do |t|
t.timestamps
@@ -90,7 +91,7 @@ module ActiveRecord
end
def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
def migrate(x)
add_timestamps :testings
end
@@ -102,17 +103,119 @@ module ActiveRecord
assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
end
- def test_legacy_migrations_get_deprecation_warning_when_run
- migration = Class.new(ActiveRecord::Migration) {
- def up
- add_column :testings, :baz, :string
- end
- }
+ def test_legacy_migrations_raises_exception_when_inherited
+ e = assert_raises(StandardError) do
+ class_eval("class LegacyMigration < ActiveRecord::Migration; end")
+ end
+ assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message)
+ end
+ end
+ end
+end
- assert_deprecated do
- migration.migrate :up
+class LegacyPrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ class LegacyPrimaryKey < ActiveRecord::Base
+ end
+
+ def setup
+ @migration = nil
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ @migration.migrate(:down) if @migration
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ LegacyPrimaryKey.reset_column_information
+ end
+
+ def test_legacy_primary_key_should_be_auto_incremented
+ @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ def change
+ create_table :legacy_primary_keys do |t|
+ t.references :legacy_ref
end
end
+ }.new
+
+ @migration.migrate(:up)
+
+ legacy_pk = LegacyPrimaryKey.columns_hash["id"]
+ assert_not legacy_pk.bigint?
+ assert_not legacy_pk.null
+
+ legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"]
+ assert_not legacy_ref.bigint?
+
+ record1 = LegacyPrimaryKey.create!
+ assert_not_nil record1.id
+
+ record1.destroy
+
+ record2 = LegacyPrimaryKey.create!
+ assert_not_nil record2.id
+ assert_operator record2.id, :>, record1.id
+ end
+
+ def test_legacy_integer_primary_key_should_not_be_auto_incremented
+ skip if current_adapter?(:SQLite3Adapter)
+
+ @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ def change
+ create_table :legacy_primary_keys, id: :integer do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_raises(ActiveRecord::NotNullViolation) do
+ LegacyPrimaryKey.create!
+ end
+
+ schema = dump_table_schema "legacy_primary_keys"
+ assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ def test_legacy_bigint_primary_key_should_be_auto_incremented
+ @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ def change
+ create_table :legacy_primary_keys, id: :bigint
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ legacy_pk = LegacyPrimaryKey.columns_hash["id"]
+ assert legacy_pk.bigint?
+ assert 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]) {
+ def change
+ create_table :legacy_primary_keys, id: :bigint do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_raises(ActiveRecord::NotNullViolation) do
+ LegacyPrimaryKey.create!
+ end
+
+ schema = dump_table_schema "legacy_primary_keys"
+ assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema
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 f14d68f12b..c4896f3d6e 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -12,9 +12,7 @@ module ActiveRecord
teardown do
%w(artists_musics musics_videos catalog).each do |table_name|
- ActiveSupport::Deprecation.silence do
- connection.drop_table table_name if connection.table_exists?(table_name)
- end
+ connection.drop_table table_name, if_exists: true
end
end
@@ -80,55 +78,66 @@ module ActiveRecord
assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns)
end
+ def test_create_join_table_respects_reference_key_type
+ connection.create_join_table :artists, :musics do |t|
+ t.references :video
+ end
+
+ artist_id, music_id, video_id = connection.columns(:artists_musics).sort_by(&:name)
+
+ assert_equal video_id.sql_type, artist_id.sql_type
+ assert_equal video_id.sql_type, music_id.sql_type
+ end
+
def test_drop_join_table
connection.create_join_table :artists, :musics
connection.drop_join_table :artists, :musics
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") }
+ assert !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"
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") }
+ assert !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
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("musics_videos") }
+ assert !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
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("catalog") }
+ assert !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"
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("catalog") }
+ assert !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 }
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") }
+ assert !connection.table_exists?("artists_musics")
end
def test_create_and_drop_join_table_with_common_prefix
with_table_cleanup do
connection.create_join_table "audio_artists", "audio_musics"
- ActiveSupport::Deprecation.silence { assert connection.table_exists?("audio_artists_musics") }
+ assert connection.table_exists?("audio_artists_musics")
connection.drop_join_table "audio_artists", "audio_musics"
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" }
+ assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't"
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index cab2069754..7762d37915 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -1,12 +1,30 @@
require "cases/helper"
-require "support/ddl_helper"
require "support/schema_dumping_helper"
+if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
+ module ActiveRecord
+ class Migration
+ class ForeignKeyInCreateTest < ActiveRecord::TestCase
+ def test_foreign_keys
+ foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
+ end
+ end
+ end
+ end
+end
+
if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ForeignKeyTest < ActiveRecord::TestCase
- include DdlHelper
include SchemaDumpingHelper
include ActiveSupport::Testing::Stream
@@ -29,10 +47,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
teardown do
- if defined?(@connection)
- @connection.drop_table "astronauts", if_exists: true
- @connection.drop_table "rockets", if_exists: true
- end
+ @connection.drop_table "astronauts", if_exists: true
+ @connection.drop_table "rockets", if_exists: true
end
def test_foreign_keys
@@ -76,20 +92,23 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_add_foreign_key_with_non_standard_primary_key
- with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do
- @connection.add_foreign_key(:astronauts, :space_shuttles,
- column: "rocket_id", primary_key: "pk", name: "custom_pk")
+ @connection.create_table :space_shuttles, id: false, force: true do |t|
+ t.bigint :pk, primary_key: true
+ end
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ @connection.add_foreign_key(:astronauts, :space_shuttles,
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
- fk = foreign_keys.first
- assert_equal "astronauts", fk.from_table
- assert_equal "space_shuttles", fk.to_table
- assert_equal "pk", fk.primary_key
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- @connection.remove_foreign_key :astronauts, name: "custom_pk"
- end
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "space_shuttles", fk.to_table
+ assert_equal "pk", fk.primary_key
+ ensure
+ @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ @connection.drop_table :space_shuttles
end
def test_add_on_delete_restrict_foreign_key
@@ -101,7 +120,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
fk = foreign_keys.first
if current_adapter?(:Mysql2Adapter)
# ON DELETE RESTRICT is the default on MySQL
- assert_equal nil, fk.on_delete
+ assert_nil fk.on_delete
else
assert_equal :restrict, fk.on_delete
end
@@ -229,7 +248,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
create_table("cities") { |t| }
create_table("houses") do |t|
- t.column :city_id, :integer
+ t.references :city
end
add_foreign_key :houses, :cities, column: "city_id"
@@ -261,7 +280,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
create_table(:schools)
create_table(:classes) do |t|
- t.column :school_id, :integer
+ t.references :school
end
add_foreign_key :classes, :schools
end
@@ -305,9 +324,11 @@ else
@connection.remove_foreign_key :clubs, :categories
end
- def test_foreign_keys_should_raise_not_implemented
- assert_raises NotImplementedError do
- @connection.foreign_keys("clubs")
+ unless current_adapter?(:SQLite3Adapter)
+ def test_foreign_keys_should_raise_not_implemented
+ assert_raises NotImplementedError do
+ @connection.foreign_keys("clubs")
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 0f975026b8..f10fcf1398 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -31,9 +31,10 @@ module ActiveRecord
connection.add_index(table_name, [:foo], name: "old_idx")
connection.rename_index(table_name, "old_idx", "new_idx")
- # if the adapter doesn't support the indexes call, pick defaults that let the test pass
- assert_not connection.index_name_exists?(table_name, "old_idx", false)
- assert connection.index_name_exists?(table_name, "new_idx", true)
+ 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
end
def test_rename_index_too_long
@@ -45,8 +46,7 @@ module ActiveRecord
}
assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message)
- # if the adapter doesn't support the indexes call, pick defaults that let the test pass
- assert connection.index_name_exists?(table_name, "old_idx", false)
+ assert connection.index_name_exists?(table_name, "old_idx")
end
def test_double_add_index
@@ -63,7 +63,7 @@ module ActiveRecord
def test_add_index_works_with_long_index_names
connection.add_index(table_name, "foo", name: good_index_name)
- assert connection.index_name_exists?(table_name, good_index_name, false)
+ assert connection.index_name_exists?(table_name, good_index_name)
connection.remove_index(table_name, name: good_index_name)
end
@@ -75,7 +75,7 @@ module ActiveRecord
}
assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message)
- assert_not connection.index_name_exists?(table_name, too_long_index_name, false)
+ assert_not connection.index_name_exists?(table_name, too_long_index_name)
connection.add_index(table_name, "foo", name: good_index_name)
end
@@ -83,7 +83,7 @@ module ActiveRecord
good_index_name = "x" * connection.index_name_length
connection.add_index(table_name, "foo", name: good_index_name, internal: true)
- assert connection.index_name_exists?(table_name, good_index_name, false)
+ assert connection.index_name_exists?(table_name, good_index_name)
connection.remove_index(table_name, name: good_index_name)
end
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
index 61f5a061b0..6970fdcc87 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -21,8 +21,6 @@ module ActiveRecord
end
def test_errors_if_pending
- @connection.expect :supports_migrations?, true
-
ActiveRecord::Migrator.stub :needs_migration?, true do
assert_raise ActiveRecord::PendingMigrationError do
@pending.call(nil)
@@ -31,22 +29,12 @@ module ActiveRecord
end
def test_checks_if_supported
- @connection.expect :supports_migrations?, true
@app.expect :call, nil, [:foo]
ActiveRecord::Migrator.stub :needs_migration?, false do
@pending.call(:foo)
end
end
-
- def test_doesnt_check_if_unsupported
- @connection.expect :supports_migrations?, false
- @app.expect :call, nil, [:foo]
-
- ActiveRecord::Migrator.stub :needs_migration?, true do
- @pending.call(:foo)
- end
- end
end
end
end
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
index 528811db49..f1ddac1ee2 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -1,9 +1,9 @@
require "cases/helper"
-if ActiveRecord::Base.connection.supports_foreign_keys?
+if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
module ActiveRecord
class Migration
- class ReferencesForeignKeyTest < ActiveRecord::TestCase
+ class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table(:testing_parents, force: true)
@@ -42,8 +42,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
test "options hash can be passed" do
@connection.change_table :testing_parents do |t|
- t.integer :other_id
- t.index :other_id, unique: true
+ t.references :other, index: { unique: true }
end
@connection.create_table :testings do |t|
t.references :testing_parent, foreign_key: { primary_key: :other_id }
@@ -61,6 +60,24 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal([["testings", "testing_parents", "parent_id"]],
fks.map { |fk| [fk.from_table, fk.to_table, fk.column] })
end
+ end
+ end
+ end
+end
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+ module ActiveRecord
+ class Migration
+ class ReferencesForeignKeyTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:testing_parents, force: true)
+ end
+
+ teardown do
+ @connection.drop_table "testings", if_exists: true
+ @connection.drop_table "testing_parents", if_exists: true
+ end
test "foreign keys cannot be added to polymorphic relations when creating the table" do
@connection.create_table :testings do |t|
@@ -92,8 +109,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
test "foreign keys accept options when changing the table" do
@connection.change_table :testing_parents do |t|
- t.integer :other_id
- t.index :other_id, unique: true
+ t.references :other, index: { unique: true }
end
@connection.create_table :testings
@connection.change_table :testings do |t|
@@ -177,18 +193,31 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
test "multiple foreign keys can be added to the same table" do
@connection.create_table :testings do |t|
- t.integer :col_1
- t.integer :col_2
+ t.references :parent1, foreign_key: { to_table: :testing_parents }
+ t.references :parent2, foreign_key: { to_table: :testing_parents }
+ end
- t.foreign_key :testing_parents, column: :col_1
- t.foreign_key :testing_parents, column: :col_2
+ fks = @connection.foreign_keys("testings").sort_by(&:column)
+
+ fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }
+ assert_equal([["testings", "testing_parents", "parent1_id"],
+ ["testings", "testing_parents", "parent2_id"]], fk_definitions)
+ end
+
+ test "multiple foreign keys can be removed to the selected one" do
+ @connection.create_table :testings do |t|
+ t.references :parent1, foreign_key: { to_table: :testing_parents }
+ t.references :parent2, foreign_key: { to_table: :testing_parents }
end
- fks = @connection.foreign_keys("testings")
+ assert_difference "@connection.foreign_keys('testings').size", -1 do
+ @connection.remove_reference :testings, :parent1, foreign_key: { to_table: :testing_parents }
+ end
+
+ fks = @connection.foreign_keys("testings").sort_by(&:column)
fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }
- assert_equal([["testings", "testing_parents", "col_1"],
- ["testings", "testing_parents", "col_2"]], fk_definitions)
+ assert_equal([["testings", "testing_parents", "parent2_id"]], fk_definitions)
end
end
end
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index df15d7cb45..06c44c8c52 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -50,6 +50,13 @@ module ActiveRecord
assert column_exists?(table_name, :taggable_type, :string, default: "Photo")
end
+ def test_does_not_share_options_with_reference_type_column
+ add_reference table_name, :taggable, type: :integer, limit: 2, polymorphic: true
+ assert column_exists?(table_name, :taggable_id, :integer, limit: 2)
+ assert column_exists?(table_name, :taggable_type, :string)
+ assert_not column_exists?(table_name, :taggable_type, :string, limit: 2)
+ end
+
def test_creates_named_index
add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id" }
assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id")
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index fc4f700916..19588d28a2 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -15,7 +15,7 @@ module ActiveRecord
end
def teardown
- ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi }
+ rename_table :octopi, :test_models if connection.table_exists? :octopi
super
end
@@ -82,7 +82,7 @@ module ActiveRecord
def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
connection.create_table :cats, id: :uuid
assert_nothing_raised { rename_table :cats, :felines }
- ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines }
+ assert connection.table_exists? :felines
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 22484ad678..da7875187a 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -43,10 +43,10 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
- ActiveRecord::Base.connection.initialize_schema_migrations_table
- ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::SchemaMigration.delete_all
- %w(things awesome_things prefix_things_suffix p_awesome_things_s ).each do |table|
+ %w(things awesome_things prefix_things_suffix p_awesome_things_s).each do |table|
Thing.connection.drop_table(table) rescue nil
end
Thing.reset_column_information
@@ -337,20 +337,20 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_schema_migrations_table_name
- original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name
+ original_schema_migrations_table_name = ActiveRecord::Base.schema_migrations_table_name
- assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "schema_migrations", ActiveRecord::SchemaMigration.table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
- assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "prefix_schema_migrations_suffix", ActiveRecord::SchemaMigration.table_name
ActiveRecord::Base.schema_migrations_table_name = "changed"
Reminder.reset_table_name
- assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "prefix_changed_suffix", ActiveRecord::SchemaMigration.table_name
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
Reminder.reset_table_name
- assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "changed", ActiveRecord::SchemaMigration.table_name
ensure
ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name
Reminder.reset_table_name
@@ -399,6 +399,7 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migrator.migrations_paths = old_path
ENV["RAILS_ENV"] = original_rails_env
ENV["RACK_ENV"] = original_rack_env
+ ActiveRecord::Migrator.up(migrations_path)
end
def test_migration_sets_internal_metadata_even_when_fully_migrated
@@ -425,6 +426,7 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migrator.migrations_paths = old_path
ENV["RAILS_ENV"] = original_rails_env
ENV["RACK_ENV"] = original_rack_env
+ ActiveRecord::Migrator.up(migrations_path)
end
def test_internal_metadata_stores_environment_when_other_data_exists
@@ -705,7 +707,7 @@ class MigrationTest < ActiveRecord::TestCase
end
end
- protected
+ private
# This is needed to isolate class_attribute assignments like `table_name_prefix`
# for each test case.
def new_isolated_reminder_class
@@ -887,7 +889,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal :datetime, column(:birthdate).type
end
- protected
+ private
def with_bulk_change_table
# Reset columns/indexes cache as we're changing the table
@@ -914,7 +916,6 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
@indexes ||= Person.connection.indexes("delete_me")
end
end # AlterTableMigrationsTest
-
end
class CopyMigrationsTest < ActiveRecord::TestCase
@@ -1132,4 +1133,21 @@ 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_migration_keys
+ assert_deprecated { ActiveRecord::Base.connection.migration_keys }
+ 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 b775bf0492..aadbc375af 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -45,10 +45,11 @@ class MigratorTest < ActiveRecord::TestCase
end
def test_migrator_with_duplicate_names
- assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
+ e = assert_raises(ActiveRecord::DuplicateMigrationNameError) do
list = [ActiveRecord::Migration.new("Chunky"), ActiveRecord::Migration.new("Chunky")]
ActiveRecord::Migrator.new(:up, list)
end
+ assert_match(/Multiple migrations have the name Chunky/, e.message)
end
def test_migrator_with_duplicate_versions
@@ -123,6 +124,67 @@ class MigratorTest < ActiveRecord::TestCase
assert_equal migration_list.last, migrations.first
end
+ def test_migrations_status
+ path = MIGRATIONS_ROOT + "/valid"
+
+ ActiveRecord::SchemaMigration.create(version: 2)
+ ActiveRecord::SchemaMigration.create(version: 10)
+
+ assert_equal [
+ ["down", "001", "Valid people have last names"],
+ ["up", "002", "We need reminders"],
+ ["down", "003", "Innocent jointable"],
+ ["up", "010", "********** NO FILE **********"],
+ ], ActiveRecord::Migrator.migrations_status(path)
+ end
+
+ def test_migrations_status_in_subdirectories
+ path = MIGRATIONS_ROOT + "/valid_with_subdirectories"
+
+ ActiveRecord::SchemaMigration.create(version: 2)
+ ActiveRecord::SchemaMigration.create(version: 10)
+
+ assert_equal [
+ ["down", "001", "Valid people have last names"],
+ ["up", "002", "We need reminders"],
+ ["down", "003", "Innocent jointable"],
+ ["up", "010", "********** NO FILE **********"],
+ ], ActiveRecord::Migrator.migrations_status(path)
+ end
+
+ def test_migrations_status_with_schema_define_in_subdirectories
+ path = MIGRATIONS_ROOT + "/valid_with_subdirectories"
+ prev_paths = ActiveRecord::Migrator.migrations_paths
+ ActiveRecord::Migrator.migrations_paths = path
+
+ ActiveRecord::Schema.define(version: 3) do
+ end
+
+ assert_equal [
+ ["up", "001", "Valid people have last names"],
+ ["up", "002", "We need reminders"],
+ ["up", "003", "Innocent jointable"],
+ ], ActiveRecord::Migrator.migrations_status(path)
+ ensure
+ ActiveRecord::Migrator.migrations_paths = prev_paths
+ end
+
+ def test_migrations_status_from_two_directories
+ paths = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"]
+
+ ActiveRecord::SchemaMigration.create(version: "20100101010101")
+ ActiveRecord::SchemaMigration.create(version: "20160528010101")
+
+ assert_equal [
+ ["down", "20090101010101", "People have hobbies"],
+ ["down", "20090101010202", "People have descriptions"],
+ ["up", "20100101010101", "Valid with timestamps people have last names"],
+ ["down", "20100201010101", "Valid with timestamps we need reminders"],
+ ["down", "20100301010101", "Valid with timestamps innocent jointable"],
+ ["up", "20160528010101", "********** NO FILE **********"],
+ ], ActiveRecord::Migrator.migrations_status(paths)
+ end
+
def test_migrator_interleaved_migrations
pass_one = [Sensor.new("One", 1)]
@@ -290,6 +352,27 @@ class MigratorTest < ActiveRecord::TestCase
assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
end
+ def test_migrator_output_when_running_multiple_migrations
+ _, migrator = migrator_class(3)
+
+ result = migrator.migrate("valid")
+ assert_equal(3, result.count)
+
+ # Nothing migrated from duplicate run
+ result = migrator.migrate("valid")
+ assert_equal(0, result.count)
+
+ result = migrator.rollback("valid")
+ assert_equal(1, result.count)
+ end
+
+ def test_migrator_output_when_running_single_migration
+ _, migrator = migrator_class(1)
+ result = migrator.run(:up, "valid", 1)
+
+ assert_equal(1, result.version)
+ end
+
def test_migrator_rollback
_, migrator = migrator_class(3)
@@ -313,9 +396,9 @@ class MigratorTest < ActiveRecord::TestCase
_, migrator = migrator_class(3)
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
- ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") }
+ assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations")
migrator.migrate("valid", 1)
- ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("schema_migrations") }
+ assert ActiveRecord::Base.connection.table_exists?("schema_migrations")
end
def test_migrator_forward
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
index 350a966d40..b9d2acbed2 100644
--- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -120,14 +120,14 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
assert_assignment_affects_records_in_target(:birds_with_add)
end
- test("Assignment updates records in target when not loaded" +
+ test("Assignment updates records in target when not loaded" \
" and callback loads target") do
assert_not @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
- test("Assignment updates records in target when loaded" +
+ test("Assignment updates records in target when loaded" \
" and callback loads target") do
@pirate.birds_with_add_load.load_target
@pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index f667e9b055..5b7e2fd008 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -90,6 +90,14 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal count, Pet.joins(:toys).where(where_args).delete_all
end
+ def test_delete_all_with_left_joins
+ where_args = { toys: { name: "Bone" } }
+ count = Pet.left_joins(:toys).where(where_args).count
+
+ assert_equal count, 1
+ assert_equal count, Pet.left_joins(:toys).where(where_args).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
@@ -131,6 +139,14 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal initial_credit + 2, a1.reload.credit_limit
end
+ def test_increment_updates_timestamps
+ topic = topics(:first)
+ topic.update_columns(updated_at: 5.minutes.ago)
+ previous_updated_at = topic.updated_at
+ topic.increment!(:replies_count, touch: true)
+ assert_operator previous_updated_at, :<, topic.reload.updated_at
+ end
+
def test_destroy_all
conditions = "author_name = 'Mary'"
topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a
@@ -222,6 +238,14 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal 41, accounts(:signals37, :reload).credit_limit
end
+ def test_decrement_updates_timestamps
+ topic = topics(:first)
+ topic.update_columns(updated_at: 5.minutes.ago)
+ previous_updated_at = topic.updated_at
+ topic.decrement!(:replies_count, touch: true)
+ assert_operator previous_updated_at, :<, topic.reload.updated_at
+ end
+
def test_create
topic = Topic.new
topic.title = "New Topic"
@@ -453,6 +477,20 @@ class PersistenceTest < ActiveRecord::TestCase
assert_nil Topic.find(2).last_read
end
+ def test_update_all_with_joins
+ where_args = { toys: { name: "Bone" } }
+ count = Pet.left_joins(:toys).where(where_args).count
+
+ assert_equal count, Pet.joins(:toys).where(where_args).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
+
+ assert_equal count, Pet.left_joins(:toys).where(where_args).update_all(name: "Bob")
+ end
+
def test_update_all_with_non_standard_table_name
assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0])
assert_equal 0, WarehouseThing.find(1).value
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index eaaf50d14f..12386635f6 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -7,6 +7,7 @@ require "models/movie"
require "models/keyboard"
require "models/mixed_case_monkey"
require "models/dashboard"
+require "models/non_primary_key"
class PrimaryKeysTest < ActiveRecord::TestCase
fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
@@ -89,6 +90,12 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal("John Doe", subscriberReloaded.name)
end
+ def test_id_column_that_is_not_primary_key
+ NonPrimaryKey.create!(id: 100)
+ actual = NonPrimaryKey.find_by(id: 100)
+ assert_match %r{<NonPrimaryKey id: 100}, actual.inspect
+ end
+
def test_find_with_more_than_one_string_key
assert_equal 2, Subscriber.find(subscribers(:first).nick, subscribers(:second).nick).length
end
@@ -113,44 +120,45 @@ class PrimaryKeysTest < ActiveRecord::TestCase
def test_delete_should_quote_pkey
assert_nothing_raised { MixedCaseMonkey.delete(1) }
end
+
def test_update_counters_should_quote_pkey_and_quote_counter_columns
assert_nothing_raised { MixedCaseMonkey.update_counters(1, fleaCount: 99) }
end
+
def test_find_with_one_id_should_quote_pkey
assert_nothing_raised { MixedCaseMonkey.find(1) }
end
+
def test_find_with_multiple_ids_should_quote_pkey
assert_nothing_raised { MixedCaseMonkey.find([1, 2]) }
end
+
def test_instance_update_should_quote_pkey
assert_nothing_raised { MixedCaseMonkey.find(1).save }
end
+
def test_instance_destroy_should_quote_pkey
assert_nothing_raised { MixedCaseMonkey.find(1).destroy }
end
- def test_supports_primary_key
- assert_nothing_raised do
- ActiveRecord::Base.connection.supports_primary_key?
- end
+ def test_deprecate_supports_primary_key
+ assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? }
end
- if ActiveRecord::Base.connection.supports_primary_key?
- def test_primary_key_returns_value_if_it_exists
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = "developers"
- end
-
- assert_equal "id", klass.primary_key
+ def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "developers"
end
- def test_primary_key_returns_nil_if_it_does_not_exist
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = "developers_projects"
- end
+ assert_equal "id", klass.primary_key
+ end
- assert_nil klass.primary_key
+ def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "developers_projects"
end
+
+ assert_nil klass.primary_key
end
def test_quoted_primary_key_after_set_primary_key
@@ -216,6 +224,43 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
end
end
+class PrimaryKeyWithAutoIncrementTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class AutoIncrement < ActiveRecord::Base
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table(:auto_increments, if_exists: true)
+ end
+
+ def test_primary_key_with_integer
+ @connection.create_table(:auto_increments, id: :integer, force: true)
+ assert_auto_incremented
+ end
+
+ def test_primary_key_with_bigint
+ @connection.create_table(:auto_increments, id: :bigint, force: true)
+ assert_auto_incremented
+ end
+
+ private
+ def assert_auto_incremented
+ record1 = AutoIncrement.create!
+ assert_not_nil record1.id
+
+ record1.destroy
+
+ record2 = AutoIncrement.create!
+ assert_not_nil record2.id
+ assert_operator record2.id, :>, record1.id
+ end
+end
+
class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
include SchemaDumpingHelper
@@ -246,6 +291,14 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
schema = dump_table_schema "barcodes"
assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema
end
+
+ if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported?
+ test "schema typed primary key column" do
+ @connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true)
+ schema = dump_table_schema("scheduled_logs")
+ assert_match %r/create_table "scheduled_logs", id: :timestamp, precision: 6/, schema
+ end
+ end
end
class CompositePrimaryKeyTest < ActiveRecord::TestCase
@@ -260,6 +313,10 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase
t.string :region
t.integer :code
end
+ @connection.create_table(:barcodes_reverse, primary_key: ["code", "region"], force: true) do |t|
+ t.string :region
+ t.integer :code
+ end
end
def teardown
@@ -270,6 +327,11 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase
assert_equal ["region", "code"], @connection.primary_keys("barcodes")
end
+ def test_composite_primary_key_out_of_order
+ skip if current_adapter?(:SQLite3Adapter)
+ assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse")
+ end
+
def test_primary_key_issues_warning
model = Class.new(ActiveRecord::Base) do
def self.table_name
@@ -282,42 +344,47 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase
assert_match(/WARNING: Active Record does not support composite primary key\./, warning)
end
- def test_collectly_dump_composite_primary_key
+ def test_dumping_composite_primary_key
schema = dump_table_schema "barcodes"
assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema
end
+
+ def test_dumping_composite_primary_key_out_of_order
+ skip if current_adapter?(:SQLite3Adapter)
+ schema = dump_table_schema "barcodes_reverse"
+ assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema
+ end
end
-if current_adapter?(:Mysql2Adapter)
- class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase
- include SchemaDumpingHelper
+class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
- self.use_transactional_tests = false
+ self.use_transactional_tests = false
- def setup
- @connection = ActiveRecord::Base.connection
- @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true)
- end
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
- def teardown
- @connection.drop_table :bigint_defaults, if_exists: true
- end
+ def teardown
+ @connection.drop_table :int_defaults, if_exists: true
+ end
- test "primary key with bigint allows default override via nil" do
- column = @connection.columns(:bigint_defaults).find { |c| c.name == "id" }
- assert column.bigint?
- assert_not column.auto_increment?
- end
+ def test_schema_dump_primary_key_integer_with_default_nil
+ skip if current_adapter?(:SQLite3Adapter)
+ @connection.create_table(:int_defaults, id: :integer, default: nil, force: true)
+ schema = dump_table_schema "int_defaults"
+ assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema
+ end
- test "schema dump primary key with bigint default nil" do
- schema = dump_table_schema "bigint_defaults"
- assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema
- end
+ def test_schema_dump_primary_key_bigint_with_default_nil
+ @connection.create_table(:int_defaults, id: :bigint, default: nil, force: true)
+ schema = dump_table_schema "int_defaults"
+ assert_match %r{create_table "int_defaults", id: :bigint, default: nil}, schema
end
end
if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
- class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
+ class PrimaryKeyIntegerTest < ActiveRecord::TestCase
include SchemaDumpingHelper
self.use_transactional_tests = false
@@ -327,46 +394,55 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
setup do
@connection = ActiveRecord::Base.connection
- if current_adapter?(:PostgreSQLAdapter)
- @connection.create_table(:widgets, id: :bigserial, force: true)
- else
- @connection.create_table(:widgets, id: :bigint, force: true)
- end
+ @pk_type = current_adapter?(:PostgreSQLAdapter) ? :serial : :integer
end
teardown do
@connection.drop_table :widgets, if_exists: true
- Widget.reset_column_information
end
- test "primary key column type with bigserial" do
- column_type = Widget.type_for_attribute(Widget.primary_key)
- assert_equal :integer, column_type.type
- assert_equal 8, column_type.limit
+ test "primary key column type with serial/integer" do
+ @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?
end
- test "primary key with bigserial are automatically numbered" do
+ test "primary key with serial/integer are automatically numbered" do
+ @connection.create_table(:widgets, id: @pk_type, force: true)
widget = Widget.create!
assert_not_nil widget.id
end
- test "schema dump primary key with bigserial" do
+ test "schema dump primary key with serial/integer" do
+ @connection.create_table(:widgets, id: @pk_type, force: true)
schema = dump_table_schema "widgets"
- if current_adapter?(:PostgreSQLAdapter)
- assert_match %r{create_table "widgets", id: :bigserial, force: :cascade}, schema
- else
- assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema
- end
+ assert_match %r{create_table "widgets", id: :#{@pk_type}, force: :cascade}, schema
end
if current_adapter?(:Mysql2Adapter)
test "primary key column type with options" do
- @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true)
+ @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_equal :integer, column.type
- assert_equal 8, column.limit
+ assert_not column.bigint?
assert column.unsigned?
+
+ schema = dump_table_schema "widgets"
+ assert_match %r{create_table "widgets", id: :integer, unsigned: true, force: :cascade}, schema
+ end
+
+ 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_equal :integer, column.type
+ assert column.bigint?
+ assert column.unsigned?
+
+ schema = dump_table_schema "widgets"
+ assert_match %r{create_table "widgets", id: :bigint, unsigned: true, force: :cascade}, schema
end
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 90054ce83d..494663eb04 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -50,32 +50,36 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_cache :off
end
- def test_query_cache_across_threads
- ActiveRecord::Base.connection_pool.connections.each do |conn|
- assert_cache :off, conn
- end
+ private def with_temporary_connection_pool
+ old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
+ new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool
- assert !ActiveRecord::Base.connection.nil?
- assert_cache :off
-
- middleware {
- assert_cache :clean
-
- Task.find 1
- assert_cache :dirty
-
- thread_1_connection = ActiveRecord::Base.connection
- ActiveRecord::Base.clear_active_connections!
- assert_cache :off, thread_1_connection
+ yield
+ ensure
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool
+ end
- started = Concurrent::Event.new
- checked = Concurrent::Event.new
+ def test_query_cache_across_threads
+ with_temporary_connection_pool do
+ begin
+ if in_memory_db?
+ # Separate connections to an in-memory database create an entirely new database,
+ # with an empty schema etc, so we just stub out this schema on the fly.
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
+ connection.create_table :tasks do |t|
+ t.datetime :starting
+ t.datetime :ending
+ end
+ end
+ ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base)
+ end
- thread_2_connection = nil
- thread = Thread.new {
- thread_2_connection = ActiveRecord::Base.connection
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ assert_cache :off, conn
+ end
- assert_equal thread_2_connection, thread_1_connection
+ assert !ActiveRecord::Base.connection.nil?
assert_cache :off
middleware {
@@ -84,29 +88,51 @@ class QueryCacheTest < ActiveRecord::TestCase
Task.find 1
assert_cache :dirty
- started.set
- checked.wait
-
+ thread_1_connection = ActiveRecord::Base.connection
ActiveRecord::Base.clear_active_connections!
- }.call({})
- }
+ assert_cache :off, thread_1_connection
+
+ started = Concurrent::Event.new
+ checked = Concurrent::Event.new
+
+ thread_2_connection = nil
+ thread = Thread.new {
+ thread_2_connection = ActiveRecord::Base.connection
- started.wait
+ assert_equal thread_2_connection, thread_1_connection
+ assert_cache :off
- thread_1_connection = ActiveRecord::Base.connection
- assert_not_equal thread_1_connection, thread_2_connection
- assert_cache :dirty, thread_2_connection
- checked.set
- thread.join
+ middleware {
+ assert_cache :clean
- assert_cache :off, thread_2_connection
- }.call({})
+ Task.find 1
+ assert_cache :dirty
- ActiveRecord::Base.connection_pool.connections.each do |conn|
- assert_cache :off, conn
+ started.set
+ checked.wait
+
+ ActiveRecord::Base.clear_active_connections!
+ }.call({})
+ }
+
+ started.wait
+
+ thread_1_connection = ActiveRecord::Base.connection
+ assert_not_equal thread_1_connection, thread_2_connection
+ assert_cache :dirty, thread_2_connection
+ checked.set
+ thread.join
+
+ assert_cache :off, thread_2_connection
+ }.call({})
+
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ assert_cache :off, conn
+ end
+ ensure
+ ActiveRecord::Base.clear_all_connections!
+ end
end
- ensure
- ActiveRecord::Base.clear_all_connections!
end
def test_middleware_delegates
@@ -202,6 +228,20 @@ class QueryCacheTest < ActiveRecord::TestCase
ActiveSupport::Notifications.unsubscribe subscriber
end
+ def test_query_cache_does_not_allow_sql_key_mutation
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|
+ payload[:sql].downcase!
+ end
+
+ assert_raises RuntimeError do
+ ActiveRecord::Base.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe subscriber
+ end
+
def test_cache_is_flat
Task.cache do
assert_queries(1) { Topic.find(1); Topic.find(1); }
@@ -246,19 +286,51 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_cache_is_not_available_when_using_a_not_connected_connection
- spec_name = Task.connection_specification_name
- conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2")
- ActiveRecord::Base.connection_handler.establish_connection(conf)
- Task.connection_specification_name = "test2"
- refute Task.connected?
+ with_temporary_connection_pool do
+ spec_name = Task.connection_specification_name
+ conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2")
+ ActiveRecord::Base.connection_handler.establish_connection(conf)
+ Task.connection_specification_name = "test2"
+ refute Task.connected?
- Task.cache do
- Task.connection # warmup postgresql connection setup queries
- assert_queries(2) { Task.find(1); Task.find(1) }
+ Task.cache do
+ begin
+ if in_memory_db?
+ Task.connection.create_table :tasks do |t|
+ t.datetime :starting
+ t.datetime :ending
+ end
+ ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base)
+ end
+ Task.connection # warmup postgresql connection setup queries
+ assert_queries(2) { Task.find(1); Task.find(1) }
+ ensure
+ ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name)
+ Task.connection_specification_name = spec_name
+ end
+ end
+ end
+ end
+
+ def test_query_cache_executes_new_queries_within_block
+ ActiveRecord::Base.connection.enable_query_cache!
+
+ # Warm up the cache by running the query
+ assert_queries(1) do
+ assert_equal 0, Post.where(title: "test").to_a.count
+ end
+
+ # Check that if the same query is run again, no queries are executed
+ assert_queries(0) do
+ assert_equal 0, Post.where(title: "test").to_a.count
+ end
+
+ ActiveRecord::Base.connection.uncached do
+ # Check that new query is executed, avoiding the cache
+ assert_queries(1) do
+ assert_equal 0, Post.where(title: "test").to_a.count
+ end
end
- ensure
- ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name)
- Task.connection_specification_name = spec_name
end
def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries
@@ -314,37 +386,44 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_query_cache_does_not_establish_connection_if_unconnected
- ActiveRecord::Base.clear_active_connections!
- refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
+ with_temporary_connection_pool do
+ ActiveRecord::Base.clear_active_connections!
+ refute 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"
- }.call({})
+ middleware {
+ refute 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"
+ refute 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
- ActiveRecord::Base.clear_active_connections!
- refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
+ with_temporary_connection_pool do
+ ActiveRecord::Base.clear_active_connections!
+ refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
- middleware {
- assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled"
- }.call({})
+ middleware {
+ assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled"
+ }.call({})
+ end
end
def test_query_caching_is_local_to_the_current_thread
- ActiveRecord::Base.clear_active_connections!
+ with_temporary_connection_pool do
+ ActiveRecord::Base.clear_active_connections!
- middleware {
- assert ActiveRecord::Base.connection_pool.query_cache_enabled
- assert ActiveRecord::Base.connection.query_cache_enabled
+ middleware {
+ assert ActiveRecord::Base.connection_pool.query_cache_enabled
+ assert ActiveRecord::Base.connection.query_cache_enabled
- Thread.new {
- refute ActiveRecord::Base.connection_pool.query_cache_enabled
- refute ActiveRecord::Base.connection.query_cache_enabled
- }.join
- }.call({})
+ Thread.new {
+ refute ActiveRecord::Base.connection_pool.query_cache_enabled
+ refute ActiveRecord::Base.connection.query_cache_enabled
+ }.join
+ }.call({})
+
+ end
end
private
@@ -374,6 +453,10 @@ end
class QueryCacheExpiryTest < ActiveRecord::TestCase
fixtures :tasks, :posts, :categories, :categories_posts
+ def teardown
+ Task.connection.clear_query_cache
+ end
+
def test_cache_gets_cleared_after_migration
# warm the cache
Post.find(1)
@@ -449,4 +532,16 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
end
end
end
+
+ test "threads use the same connection" do
+ @connection_1 = ActiveRecord::Base.connection.object_id
+
+ thread_a = Thread.new do
+ @connection_2 = ActiveRecord::Base.connection.object_id
+ end
+
+ thread_a.join
+
+ assert_equal @connection_1, @connection_2
+ end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 05b71638c1..f260d043e4 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -82,46 +82,46 @@ module ActiveRecord
end
def test_quote_with_quoted_id
- assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil)
+ assert_deprecated { assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1)) }
end
def test_quote_nil
- assert_equal "NULL", @quoter.quote(nil, nil)
+ assert_equal "NULL", @quoter.quote(nil)
end
def test_quote_true
- assert_equal @quoter.quoted_true, @quoter.quote(true, nil)
+ assert_equal @quoter.quoted_true, @quoter.quote(true)
end
def test_quote_false
- assert_equal @quoter.quoted_false, @quoter.quote(false, nil)
+ assert_equal @quoter.quoted_false, @quoter.quote(false)
end
def test_quote_float
float = 1.2
- assert_equal float.to_s, @quoter.quote(float, nil)
+ assert_equal float.to_s, @quoter.quote(float)
end
def test_quote_integer
integer = 1
- assert_equal integer.to_s, @quoter.quote(integer, nil)
+ assert_equal integer.to_s, @quoter.quote(integer)
end
def test_quote_bignum
bignum = 1 << 100
- assert_equal bignum.to_s, @quoter.quote(bignum, nil)
+ assert_equal bignum.to_s, @quoter.quote(bignum)
end
def test_quote_bigdecimal
bigdec = BigDecimal.new((1 << 100).to_s)
- assert_equal bigdec.to_s("F"), @quoter.quote(bigdec, nil)
+ assert_equal bigdec.to_s("F"), @quoter.quote(bigdec)
end
def test_dates_and_times
@quoter.extend(Module.new { def quoted_date(value) "lol" end })
- assert_equal "'lol'", @quoter.quote(Date.today, nil)
- assert_equal "'lol'", @quoter.quote(Time.now, nil)
- assert_equal "'lol'", @quoter.quote(DateTime.now, nil)
+ assert_equal "'lol'", @quoter.quote(Date.today)
+ assert_equal "'lol'", @quoter.quote(Time.now)
+ assert_equal "'lol'", @quoter.quote(DateTime.now)
end
def test_quoting_classes
@@ -131,7 +131,7 @@ module ActiveRecord
def test_crazy_object
crazy = Object.new
e = assert_raises(TypeError) do
- @quoter.quote(crazy, nil)
+ @quoter.quote(crazy)
end
assert_equal "can't quote Object", e.message
end
@@ -150,6 +150,62 @@ module ActiveRecord
end
end
+ class TypeCastingTest < ActiveRecord::TestCase
+ def setup
+ @conn = ActiveRecord::Base.connection
+ end
+
+ def test_type_cast_symbol
+ assert_equal "foo", @conn.type_cast(:foo)
+ end
+
+ def test_type_cast_date
+ date = Date.today
+ expected = @conn.quoted_date(date)
+ assert_equal expected, @conn.type_cast(date)
+ end
+
+ def test_type_cast_time
+ time = Time.now
+ expected = @conn.quoted_date(time)
+ assert_equal expected, @conn.type_cast(time)
+ end
+
+ def test_type_cast_numeric
+ assert_equal 10, @conn.type_cast(10)
+ assert_equal 2.2, @conn.type_cast(2.2)
+ end
+
+ def test_type_cast_nil
+ assert_nil @conn.type_cast(nil)
+ end
+
+ def test_type_cast_unknown_should_raise_error
+ 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
def setup
@connection = ActiveRecord::Base.connection
@@ -165,5 +221,32 @@ module ActiveRecord
assert_predicate @connection.type_cast(false), :frozen?
end
end
+
+ if subsecond_precision_supported?
+ class QuoteARBaseTest < ActiveRecord::TestCase
+ class DatetimePrimaryKey < ActiveRecord::Base
+ end
+
+ def setup
+ @time = ::Time.utc(2017, 2, 14, 12, 34, 56, 789999)
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :datetime_primary_keys, id: :datetime, precision: 3, force: true
+ end
+
+ def teardown
+ @connection.drop_table :datetime_primary_keys, if_exists: true
+ end
+
+ def test_quote_ar_object
+ value = DatetimePrimaryKey.new(id: @time)
+ assert_equal "'2017-02-14 12:34:56.789000'", @connection.quote(value)
+ end
+
+ def test_type_cast_ar_object
+ value = DatetimePrimaryKey.new(id: @time)
+ assert_equal "2017-02-14 12:34:56.789000", @connection.type_cast(value)
+ end
+ end
+ end
end
end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index a93061b516..24b678310d 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -10,7 +10,7 @@ require "models/person"
require "models/ship"
class ReadOnlyTest < ActiveRecord::TestCase
- fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
+ fixtures :authors, :author_addresses, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
def test_cant_save_readonly_record
dev = Developer.find(1)
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 5dac3d064b..c1c2efb9c8 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -86,8 +86,8 @@ class ReflectionTest < ActiveRecord::TestCase
column = @first.column_for_attribute("attribute_that_doesnt_exist")
assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
assert_equal "attribute_that_doesnt_exist", column.name
- assert_equal nil, column.sql_type
- assert_equal nil, column.type
+ assert_nil column.sql_type
+ assert_nil column.type
end
def test_non_existent_types_are_identity_types
@@ -100,7 +100,13 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_reflection_klass_for_nested_class_name
- reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { class_name: "MyApplication::Business::Company" }, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(
+ :has_many,
+ nil,
+ nil,
+ { class_name: "MyApplication::Business::Company" },
+ Customer
+ )
assert_nothing_raised do
assert_equal MyApplication::Business::Company, reflection.klass
end
@@ -252,7 +258,9 @@ class ReflectionTest < ActiveRecord::TestCase
[Post.reflect_on_association(:first_taggings).scope],
[Author.reflect_on_association(:misc_posts).scope]
]
- actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain
+ actual = assert_deprecated do
+ Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain
+ end
assert_equal expected, actual
expected = [
@@ -264,7 +272,9 @@ class ReflectionTest < ActiveRecord::TestCase
[],
[]
]
- actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
+ actual = assert_deprecated do
+ Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
+ end
assert_equal expected, actual
end
@@ -325,6 +335,15 @@ 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 }
@@ -389,15 +408,27 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_through_reflection_scope_chain_does_not_modify_other_reflections
- orig_conds = Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
- assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect
+ 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?
+ end
+ end
+
def test_join_table
category = Struct.new(:table_name, :pluralize_table_names).new("categories", true)
product = Struct.new(:table_name, :pluralize_table_names).new("products", true)
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index d2382b9bb2..8cb7b82015 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -32,7 +32,8 @@ module ActiveRecord
:exclude?, :find_all, :flat_map, :group_by, :include?, :length,
:map, :none?, :one?, :partition, :reject, :reverse,
:sample, :second, :sort, :sort_by, :third,
- :to_ary, :to_set, :to_xml, :to_yaml, :join
+ :to_ary, :to_set, :to_xml, :to_yaml, :join,
+ :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json
]
ARRAY_DELEGATES.each do |method|
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index 278dac8171..9e95149ede 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -8,7 +8,7 @@ require "models/project"
require "models/rating"
class RelationMergingTest < ActiveRecord::TestCase
- fixtures :developers, :comments, :authors, :posts
+ fixtures :developers, :comments, :authors, :author_addresses, :posts
def test_relation_merging
devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order("id ASC").where("id < 3"))
@@ -114,7 +114,7 @@ class RelationMergingTest < ActiveRecord::TestCase
end
class MergingDifferentRelationsTest < ActiveRecord::TestCase
- fixtures :posts, :authors, :developers
+ fixtures :posts, :authors, :author_addresses, :developers
test "merging where relations" do
hello_by_bob = Post.where(body: "hello").joins(:author).
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index 966ae83a3f..4f92f71a09 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -3,7 +3,7 @@ require "models/post"
module ActiveRecord
class RelationMutationTest < ActiveRecord::TestCase
- class FakeKlass < Struct.new(:table_name, :name)
+ FakeKlass = Struct.new(:table_name, :name) do
extend ActiveRecord::Delegation::DelegateCache
inherited self
@@ -90,7 +90,7 @@ module ActiveRecord
assert_equal [], relation.extending_values
end
- (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method|
+ (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method|
test "##{method}!" do
assert relation.public_send("#{method}!", :foo).equal?(relation)
assert_equal :foo, relation.public_send("#{method}_value")
@@ -108,7 +108,7 @@ module ActiveRecord
end
test "#reorder!" do
- @relation = self.relation.order("foo")
+ @relation = relation.order("foo")
assert relation.reorder!("bar").equal?(relation)
assert_equal ["bar"], relation.order_values
@@ -161,22 +161,6 @@ module ActiveRecord
test "distinct!" do
relation.distinct! :foo
assert_equal :foo, relation.distinct_value
-
- assert_deprecated do
- assert_equal :foo, relation.uniq_value # deprecated access
- end
- end
-
- test "uniq! was replaced by distinct!" do
- assert_deprecated(/use distinct! instead/) do
- relation.uniq! :foo
- end
-
- assert_deprecated(/use distinct_value instead/) do
- assert_equal :foo, relation.uniq_value # deprecated access
- end
-
- assert_equal :foo, relation.distinct_value
end
end
end
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
index 2796595523..abb7ca72dd 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -79,7 +79,7 @@ module ActiveRecord
expected = Post.where("id = 1 or id = 2").to_a
p = Post.where("id = 1")
p.load
- assert_equal p.loaded?, true
+ assert_equal true, p.loaded?
assert_equal expected, p.or(Post.where("id = 2")).to_a
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index ed8ffddcf5..c7b2ac90fb 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -15,7 +15,7 @@ require "models/vertex"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates
+ fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates
def test_where_copies_bind_params
author = authors(:david)
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index d5af0cc9a5..5fb32270b7 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -6,9 +6,9 @@ require "models/rating"
module ActiveRecord
class RelationTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :authors
+ fixtures :posts, :comments, :authors, :author_addresses
- class FakeKlass < Struct.new(:table_name, :name)
+ FakeKlass = Struct.new(:table_name, :name) do
extend ActiveRecord::Delegation::DelegateCache
inherited self
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 96833ad428..8c06b1537a 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -22,7 +22,7 @@ require "models/categorization"
require "models/edge"
class RelationTest < ActiveRecord::TestCase
- fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
+ fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
:tags, :taggings, :cars, :minivans
class TopicWithCallbacks < ActiveRecord::Base
@@ -112,7 +112,7 @@ class RelationTest < ActiveRecord::TestCase
def test_loaded_first
topics = Topic.all.order("id ASC")
- topics.to_a # force load
+ topics.load # force load
assert_no_queries do
assert_equal "The First Topic", topics.first.title
@@ -123,7 +123,7 @@ class RelationTest < ActiveRecord::TestCase
def test_loaded_first_with_limit
topics = Topic.all.order("id ASC")
- topics.to_a # force load
+ topics.load # force load
assert_no_queries do
assert_equal ["The First Topic",
@@ -136,7 +136,7 @@ class RelationTest < ActiveRecord::TestCase
def test_first_get_more_than_available
topics = Topic.all.order("id ASC")
unloaded_first = topics.first(10)
- topics.to_a # force load
+ topics.load # force load
assert_no_queries do
loaded_first = topics.first(10)
@@ -218,17 +218,34 @@ class RelationTest < ActiveRecord::TestCase
assert_equal topics(:fifth).title, topics.first.title
end
- def test_finding_with_reverted_assoc_order
+ def test_finding_with_arel_assoc_order
+ topics = Topic.order(Arel.sql("id") => :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_reversed_assoc_order
topics = Topic.order(id: :asc).reverse_order
assert_equal 5, topics.to_a.size
assert_equal topics(:fifth).title, topics.first.title
end
+ def test_finding_with_reversed_arel_assoc_order
+ topics = Topic.order(Arel.sql("id") => :asc).reverse_order
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
def test_reverse_order_with_function
topics = Topic.order("length(title)").reverse_order
assert_equal topics(:second).title, topics.first.title
end
+ def test_reverse_arel_assoc_order_with_function
+ topics = Topic.order(Arel.sql("length(title)") => :asc).reverse_order
+ assert_equal topics(:second).title, topics.first.title
+ end
+
def test_reverse_order_with_function_other_predicates
topics = Topic.order("author_name, length(title), id").reverse_order
assert_equal topics(:second).title, topics.first.title
@@ -251,6 +268,12 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_reverse_arel_assoc_order_with_multiargument_function
+ assert_nothing_raised do
+ Topic.order(Arel.sql("REPLACE(title, '', '')") => :asc).reverse_order
+ end
+ end
+
def test_reverse_order_with_nulls_first_or_last
assert_raises(ActiveRecord::IrreversibleOrderError) do
Topic.order("title NULLS FIRST").reverse_order
@@ -442,7 +465,7 @@ class RelationTest < ActiveRecord::TestCase
assert_no_queries(ignore_none: false) do
assert_equal 0, Developer.none.count
assert_equal 0, Developer.none.calculate(:count, nil)
- assert_equal nil, Developer.none.calculate(:average, "salary")
+ assert_nil Developer.none.calculate(:average, "salary")
end
end
@@ -485,28 +508,28 @@ class RelationTest < ActiveRecord::TestCase
def test_null_relation_average
ac = Aircraft.new
assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
- assert_equal nil, ac.engines.average(:id)
+ assert_nil ac.engines.average(:id)
ac.save
assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
- assert_equal nil, ac.engines.average(:id)
+ assert_nil ac.engines.average(:id)
end
def test_null_relation_minimum
ac = Aircraft.new
assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
- assert_equal nil, ac.engines.minimum(:id)
+ assert_nil ac.engines.minimum(:id)
ac.save
assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
- assert_equal nil, ac.engines.minimum(:id)
+ assert_nil ac.engines.minimum(:id)
end
def test_null_relation_maximum
ac = Aircraft.new
assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
- assert_equal nil, ac.engines.maximum(:id)
+ assert_nil ac.engines.maximum(:id)
ac.save
assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
- assert_equal nil, ac.engines.maximum(:id)
+ assert_nil ac.engines.maximum(:id)
end
def test_null_relation_in_where_condition
@@ -840,15 +863,6 @@ class RelationTest < ActiveRecord::TestCase
assert_equal author, authors.first
end
- class Mary < Author; end
-
- def test_find_by_classname
- Author.create!(name: Mary.name)
- assert_deprecated do
- assert_equal 1, Author.where(name: Mary).size
- end
- end
-
def test_find_by_id_with_list_of_ar
author = Author.first
authors = Author.find_by_id([author])
@@ -1013,12 +1027,6 @@ class RelationTest < ActiveRecord::TestCase
assert davids.loaded?
end
- def test_destroy_all_with_conditions_is_deprecated
- assert_deprecated do
- assert_difference("Author.count", -1) { Author.destroy_all(name: "David") }
- end
- end
-
def test_delete_all
davids = Author.where(name: "David")
@@ -1026,12 +1034,6 @@ class RelationTest < ActiveRecord::TestCase
assert ! davids.loaded?
end
- def test_delete_all_with_conditions_is_deprecated
- assert_deprecated do
- assert_difference("Author.count", -1) { Author.delete_all(name: "David") }
- end
- end
-
def test_delete_all_loaded
davids = Author.where(name: "David")
@@ -1153,7 +1155,7 @@ class RelationTest < ActiveRecord::TestCase
assert ! posts.loaded?
best_posts = posts.where(comments_count: 0)
- best_posts.to_a # force load
+ best_posts.load # force load
assert_no_queries { assert_equal 9, best_posts.size }
end
@@ -1164,7 +1166,7 @@ class RelationTest < ActiveRecord::TestCase
assert ! posts.loaded?
best_posts = posts.where(comments_count: 0)
- best_posts.to_a # force load
+ best_posts.load # force load
assert_no_queries { assert_equal 9, best_posts.size }
end
@@ -1174,7 +1176,7 @@ class RelationTest < ActiveRecord::TestCase
assert_no_queries { assert_equal 0, posts.size }
assert ! posts.loaded?
- posts.to_a # force load
+ posts.load # force load
assert_no_queries { assert_equal 0, posts.size }
end
@@ -1203,7 +1205,7 @@ class RelationTest < ActiveRecord::TestCase
assert ! no_posts.loaded?
best_posts = posts.where(comments_count: 0)
- best_posts.to_a # force load
+ best_posts.load # force load
assert_no_queries { assert_equal false, best_posts.empty? }
end
@@ -1638,9 +1640,9 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "David", topic2.reload.author_name
end
- def test_update_on_relation_passing_active_record_object_is_deprecated
+ def test_update_on_relation_passing_active_record_object_is_not_permitted
topic = Topic.create!(title: "Foo", author_name: nil)
- assert_deprecated(/update/) do
+ assert_raises(ArgumentError) do
Topic.where(id: topic.id).update(topic, title: "Bar")
end
end
@@ -1654,17 +1656,11 @@ class RelationTest < ActiveRecord::TestCase
assert_equal ["Foo", "Foo"], query.map(&:name)
assert_sql(/DISTINCT/) do
assert_equal ["Foo"], query.distinct.map(&:name)
- assert_deprecated { assert_equal ["Foo"], query.uniq.map(&:name) }
end
assert_sql(/DISTINCT/) do
assert_equal ["Foo"], query.distinct(true).map(&:name)
- assert_deprecated { assert_equal ["Foo"], query.uniq(true).map(&:name) }
end
assert_equal ["Foo", "Foo"], query.distinct(true).distinct(false).map(&:name)
-
- assert_deprecated do
- assert_equal ["Foo", "Foo"], query.uniq(true).uniq(false).map(&:name)
- end
end
def test_doesnt_add_having_values_if_options_are_blank
@@ -1814,7 +1810,7 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by returns nil if the record is missing" do
- assert_equal nil, Post.all.find_by("1 = 0")
+ assert_nil Post.all.find_by("1 = 0")
end
test "find_by doesn't have implicit ordering" do
@@ -1962,6 +1958,26 @@ class RelationTest < ActiveRecord::TestCase
assert !Post.all.respond_to?(:by_lifo)
end
+ def test_unscope_with_subquery
+ p1 = Post.where(id: 1)
+ p2 = Post.where(id: 2)
+
+ assert_not_equal p1, p2
+
+ comments = Comment.where(post: p1).unscope(where: :post_id).where(post: p2)
+
+ assert_not_equal p1.first.comments, comments
+ assert_equal p2.first.comments, comments
+ end
+
+ def test_unscope_specific_where_value
+ posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day")
+
+ assert_equal 1, posts.count
+ assert_equal 1, posts.unscope(where: :title).count
+ assert_equal 1, posts.unscope(where: :body).count
+ end
+
def test_unscope_removes_binds
left = Post.where(id: Arel::Nodes::BindParam.new)
column = Post.columns_hash["id"]
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 23bcb0af1e..72f09186e2 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -152,11 +152,15 @@ class SanitizeTest < ActiveRecord::TestCase
end
def test_bind_record
- o = Struct.new(:quoted_id).new(1)
- assert_equal "1", bind("?", o)
+ o = Class.new {
+ def quoted_id
+ 1
+ end
+ }.new
+ assert_deprecated { assert_equal "1", bind("?", o) }
os = [o] * 3
- assert_equal "1,1,1", bind("?", os)
+ assert_deprecated { assert_equal "1,1,1", bind("?", os) }
end
def test_named_bind_with_postgresql_type_casts
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 007f976f2e..fccba4738f 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -148,12 +148,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_4.*limit: 4}, output
end
- if current_adapter?(:SQLite3Adapter)
- assert_match %r{c_int_5.*limit: 5}, output
- assert_match %r{c_int_6.*limit: 6}, output
- assert_match %r{c_int_7.*limit: 7}, output
- assert_match %r{c_int_8.*limit: 8}, output
- elsif current_adapter?(:OracleAdapter)
+ if current_adapter?(:SQLite3Adapter, :OracleAdapter)
assert_match %r{c_int_5.*limit: 5}, output
assert_match %r{c_int_6.*limit: 6}, output
assert_match %r{c_int_7.*limit: 7}, output
@@ -183,24 +178,20 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dumps_index_columns_in_right_order
- index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip
+ 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 }, using: :btree', index_definition
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition
elsif current_adapter?(:Mysql2Adapter)
- assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, using: :btree', index_definition
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition
else
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition
end
end
def test_schema_dumps_partial_indices
- index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip
- if current_adapter?(:PostgreSQLAdapter)
- assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
- elsif current_adapter?(:Mysql2Adapter)
- assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
- elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
- assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip
+ if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
+ assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition
else
assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition
end
@@ -253,9 +244,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dumps_index_type
- output = standard_dump
- assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
- assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output
+ output = dump_table_schema "key_tests"
+ assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext$}, output
+ assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza"$}, output
end
end
@@ -266,23 +257,34 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:PostgreSQLAdapter)
def test_schema_dump_includes_bigint_default
- output = standard_dump
+ output = dump_table_schema "defaults"
assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output
end
def test_schema_dump_includes_limit_on_array_type
- output = standard_dump
+ output = dump_table_schema "bigint_array"
assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output
end
def test_schema_dump_allows_array_of_decimal_defaults
- output = standard_dump
+ output = dump_table_schema "bigint_array"
assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output
end
def test_schema_dump_expression_indices
- index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_expression_index/).first.strip
- assert_equal 't.index "lower((name)::text)", name: "company_expression_index", using: :btree', index_definition
+ 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
+ end
+
+ def test_schema_dump_interval_type
+ output = dump_table_schema "postgresql_times"
+ assert_match %r{t\.interval\s+"time_interval"$}, output
+ assert_match %r{t\.interval\s+"scaled_time_interval",\s+precision: 6$}, output
+ end
+
+ def test_schema_dump_oid_type
+ output = dump_table_schema "postgresql_oids"
+ assert_match %r{t\.oid\s+"obj_id"$}, output
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -346,9 +348,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
create_table("dogs") do |t|
t.column :name, :string
- t.column :owner_id, :integer
+ t.references :owner
t.index [:name]
- t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys?
+ t.foreign_key :dog_owners, column: "owner_id"
end
end
def down
@@ -420,11 +422,12 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase
setup do
@connection = ActiveRecord::Base.connection
- @connection.create_table :defaults, force: true do |t|
+ @connection.create_table :dump_defaults, force: true do |t|
t.string :string_with_default, default: "Hello!"
t.date :date_with_default, default: "2014-06-05"
t.datetime :datetime_with_default, default: "2014-06-05 07:17:04"
t.time :time_with_default, default: "07:17:04"
+ t.decimal :decimal_with_default, default: "1234567890.0123456789", precision: 20, scale: 10
end
if current_adapter?(:PostgreSQLAdapter)
@@ -436,17 +439,17 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase
end
teardown do
- return unless @connection
- @connection.drop_table "defaults", if_exists: true
+ @connection.drop_table "dump_defaults", if_exists: true
end
def test_schema_dump_defaults_with_universally_supported_types
- output = dump_table_schema("defaults")
+ output = dump_table_schema("dump_defaults")
assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output
- assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output
- assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output
- assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output
+ assert_match %r{t\.date\s+"date_with_default",\s+default: "2014-06-05"}, output
+ assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: "2014-06-05 07:17:04"}, output
+ assert_match %r{t\.time\s+"time_with_default",\s+default: "2000-01-01 07:17:04"}, output
+ assert_match %r{t\.decimal\s+"decimal_with_default",\s+precision: 20,\s+scale: 10,\s+default: "1234567890.0123456789"}, output
end
def test_schema_dump_with_float_column_infinity_default
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index b3dc979720..a6c22ac672 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -5,6 +5,7 @@ require "models/developer"
require "models/computer"
require "models/vehicle"
require "models/cat"
+require "concurrent/atomic/cyclic_barrier"
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers, :posts, :comments
@@ -50,7 +51,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_default_scope_with_conditions_string
assert_equal Developer.where(name: "David").map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
- assert_equal nil, DeveloperCalledDavid.create!.name
+ assert_nil DeveloperCalledDavid.create!.name
end
def test_default_scope_with_conditions_hash
@@ -58,17 +59,6 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal "Jamis", DeveloperCalledJamis.create!.name
end
- unless in_memory_db?
- def test_default_scoping_with_threads
- 2.times do
- Thread.new {
- assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC"
- DeveloperOrderedBySalary.connection.close
- }.join
- end
- end
- end
-
def test_default_scope_with_inheritance
wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash
assert_equal "Jamis", wheres["name"]
@@ -314,7 +304,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_create_attribute_overwrites_default_values
- assert_equal nil, PoorDeveloperCalledJamis.create!(salary: nil).salary
+ assert_nil PoorDeveloperCalledJamis.create!(salary: nil).salary
assert_equal 50000, PoorDeveloperCalledJamis.create!(name: "David").salary
end
@@ -432,24 +422,6 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id)
end
- unless in_memory_db?
- def test_default_scope_is_threadsafe
- threads = []
- assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
-
- threads << Thread.new do
- Thread.current[:long_default_scope] = true
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- ThreadsafeDeveloper.connection.close
- end
- threads << Thread.new do
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- ThreadsafeDeveloper.connection.close
- end
- threads.each(&:join)
- end
- end
-
test "additional conditions are ANDed with the default scope" do
scope = DeveloperCalledJamis.where(name: "David")
assert_equal 2, scope.where_clause.ast.children.length
@@ -498,3 +470,37 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_match gender_pattern, Lion.female.to_sql
end
end
+
+class DefaultScopingWithThreadTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ def test_default_scoping_with_threads
+ 2.times do
+ Thread.new {
+ assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC"
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
+ end
+
+ def test_default_scope_is_threadsafe
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ barrier_1 = Concurrent::CyclicBarrier.new(2)
+ barrier_2 = Concurrent::CyclicBarrier.new(2)
+
+ threads << Thread.new do
+ Thread.current[:default_scope_delay] = -> { barrier_1.wait; barrier_2.wait }
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads << Thread.new do
+ Thread.current[:default_scope_delay] = -> { barrier_2.wait }
+ barrier_1.wait
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads.each(&:join)
+ end
+end unless in_memory_db?
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 995ff4dfc5..0c2cffe0d3 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -23,8 +23,8 @@ class NamedScopingTest < ActiveRecord::TestCase
all_posts = Topic.base
assert_queries(1) do
- all_posts.collect
- all_posts.collect
+ all_posts.collect { true }
+ all_posts.collect { true }
end
end
@@ -161,13 +161,13 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_first_and_last_should_allow_integers_for_limit
- assert_equal Topic.base.first(2), Topic.base.to_a.first(2)
+ assert_equal Topic.base.first(2), Topic.base.order("id").to_a.first(2)
assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
end
def test_first_and_last_should_not_use_query_when_results_are_loaded
topics = Topic.base
- topics.reload # force load
+ topics.load # force load
assert_no_queries do
topics.first
topics.last
@@ -178,7 +178,7 @@ class NamedScopingTest < ActiveRecord::TestCase
topics = Topic.base
assert_queries(2) do
topics.empty? # use count query
- topics.collect # force load
+ topics.load # force load
topics.empty? # use loaded (no query)
end
end
@@ -187,7 +187,7 @@ class NamedScopingTest < ActiveRecord::TestCase
topics = Topic.base
assert_queries(2) do
topics.any? # use count query
- topics.collect # force load
+ topics.load # force load
topics.any? # use loaded (no query)
end
end
@@ -203,7 +203,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_any_should_not_fire_query_if_scope_loaded
topics = Topic.base
- topics.collect # force load
+ topics.load # force load
assert_no_queries { assert topics.any? }
end
@@ -217,7 +217,7 @@ class NamedScopingTest < ActiveRecord::TestCase
topics = Topic.base
assert_queries(2) do
topics.many? # use count query
- topics.collect # force load
+ topics.load # force load
topics.many? # use loaded (no query)
end
end
@@ -233,7 +233,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_not_fire_query_if_scope_loaded
topics = Topic.base
- topics.collect # force load
+ topics.load # force load
assert_no_queries { assert topics.many? }
end
@@ -384,7 +384,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_size_should_use_length_when_results_are_loaded
topics = Topic.base
- topics.reload # force load
+ topics.load # force load
assert_no_queries do
topics.size # use loaded (no query)
end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index 47310a151e..3fbff7664b 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -10,7 +10,7 @@ require "models/person"
require "models/reference"
class RelationScopingTest < ActiveRecord::TestCase
- fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
+ fixtures :authors, :author_addresses, :developers, :projects, :comments, :posts, :developers_projects
setup do
developers(:david)
@@ -238,7 +238,7 @@ class RelationScopingTest < ActiveRecord::TestCase
end
class NestedRelationScopingTest < ActiveRecord::TestCase
- fixtures :authors, :developers, :projects, :comments, :posts
+ fixtures :authors, :author_addresses, :developers, :projects, :comments, :posts
def test_merge_options
Developer.where("salary = 80000").scoping do
@@ -277,7 +277,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
assert_equal "David", Developer.first.name
Developer.unscoped.where("name = 'Maiha'") do
- assert_equal nil, Developer.first
+ assert_nil Developer.first
end
# ensure that scoping is restored
diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb
index eda0229c26..7b9cbee40a 100644
--- a/activerecord/test/cases/secure_token_test.rb
+++ b/activerecord/test/cases/secure_token_test.rb
@@ -27,6 +27,6 @@ class SecureTokenTest < ActiveRecord::TestCase
@user.token = "custom-secure-token"
@user.save
- assert_equal @user.token, "custom-secure-token"
+ assert_equal "custom-secure-token", @user.token
end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 1ccbf3ed4a..673392b4c4 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -185,14 +185,14 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = Topic.new(content: true)
assert topic.save
topic = topic.reload
- assert_equal topic.content, true
+ assert_equal true, topic.content
end
def test_serialized_boolean_value_false
topic = Topic.new(content: false)
assert topic.save
topic = topic.reload
- assert_equal topic.content, false
+ assert_equal false, topic.content
end
def test_serialize_with_coder
@@ -211,7 +211,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic.save!
topic.reload
assert_kind_of some_class, topic.content
- assert_equal topic.content, some_class.new("my value")
+ assert_equal some_class.new("my value"), topic.content
end
def test_serialize_attribute_via_select_method_when_time_zone_available
@@ -240,6 +240,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal [], light.long_state
end
+ def test_unexpected_serialized_type
+ Topic.serialize :content, Hash
+ topic = Topic.create!(content: { zomg: true })
+
+ Topic.serialize :content, Array
+
+ topic.reload
+ error = assert_raise(ActiveRecord::SerializationTypeMismatch) do
+ topic.content
+ end
+ expected = "can't load `content`: was supposed to be a Array, but was a Hash. -- {:zomg=>true}"
+ assert_equal expected, error.to_s
+ end
+
def test_serialized_column_should_unserialize_after_update_column
t = Topic.create(content: "first")
assert_equal("first", t.content)
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index f45f63c68e..fab3648564 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -105,5 +105,31 @@ module ActiveRecord
refute_equal book, other_book
end
+
+ def test_find_by_does_not_use_statement_cache_if_table_name_is_changed
+ book = Book.create(name: "my book")
+
+ Book.find_by(name: book.name) # warming the statement cache.
+
+ # changing the table name should change the query that is not cached.
+ Book.table_name = :birds
+ assert_nil Book.find_by(name: book.name)
+ ensure
+ Book.table_name = :books
+ end
+
+ def test_find_does_not_use_statement_cache_if_table_name_is_changed
+ book = Book.create(name: "my book")
+
+ Book.find(book.id) # warming the statement cache.
+
+ # changing the table name should change the query that is not cached.
+ Book.table_name = :birds
+ assert_raise ActiveRecord::RecordNotFound do
+ Book.find(book.id)
+ end
+ ensure
+ Book.table_name = :books
+ end
end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index d03231e711..baa41a3a47 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -61,7 +61,7 @@ module ActiveRecord
instance = klazz.new
klazz.stubs(:new).returns instance
- instance.expects(:structure_dump).with("awesome-file.sql")
+ 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")
@@ -85,11 +85,23 @@ module ActiveRecord
end
end
+ class DatabaseTasksDumpSchemaCacheTest < ActiveRecord::TestCase
+ def test_dump_schema_cache
+ path = "/tmp/my_schema_cache.yml"
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path)
+ assert File.file?(path)
+ ensure
+ FileUtils.rm_rf(path)
+ end
+ end
+
class DatabaseTasksCreateAllTest < ActiveRecord::TestCase
def setup
@configurations = { "development" => { "database" => "my-db" } }
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
+ # To refrain from connecting to a newly created empty DB in sqlite3_mem tests
+ ActiveRecord::Base.connection_handler.stubs(:establish_connection)
end
def test_ignores_configurations_without_databases
@@ -338,6 +350,14 @@ module ActiveRecord
ENV["VERBOSE"], ENV["VERSION"] = verbose, version
end
+ def test_migrate_raise_error_on_empty_version
+ version = ENV["VERSION"]
+ ENV["VERSION"] = ""
+ assert_raise(RuntimeError, "Empty VERSION provided") { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ ensure
+ ENV["VERSION"] = version
+ end
+
def test_migrate_clears_schema_cache_afterward
ActiveRecord::Base.expects(:clear_cache!)
ActiveRecord::Tasks::DatabaseTasks.migrate
@@ -411,7 +431,7 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_structure_dump") do
- eval("@#{v}").expects(:structure_dump).with("awesome-file.sql")
+ eval("@#{v}").expects(:structure_dump).with("awesome-file.sql", nil)
ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql")
end
end
@@ -422,7 +442,7 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_structure_load") do
- eval("@#{v}").expects(:structure_load).with("awesome-file.sql")
+ eval("@#{v}").expects(:structure_load).with("awesome-file.sql", nil)
ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql")
end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index dbe935808e..f30e0958c3 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -59,7 +59,7 @@ if current_adapter?(:Mysql2Adapter)
def test_when_database_created_successfully_outputs_info_to_stdout
ActiveRecord::Tasks::DatabaseTasks.create @configuration
- assert_equal $stdout.string, "Created database 'my-app-db'\n"
+ assert_equal "Created database 'my-app-db'\n", $stdout.string
end
def test_create_when_database_exists_outputs_info_to_stderr
@@ -69,7 +69,7 @@ if current_adapter?(:Mysql2Adapter)
ActiveRecord::Tasks::DatabaseTasks.create @configuration
- assert_equal $stderr.string, "Database 'my-app-db' already exists\n"
+ assert_equal "Database 'my-app-db' already exists\n", $stderr.string
end
end
@@ -205,7 +205,7 @@ if current_adapter?(:Mysql2Adapter)
def test_when_database_dropped_successfully_outputs_info_to_stdout
ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- assert_equal $stdout.string, "Dropped database 'my-app-db'\n"
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
end
end
@@ -294,6 +294,17 @@ if current_adapter?(:Mysql2Adapter)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
end
+ def test_structure_dump_with_extra_flags
+ filename = "awesome-file.sql"
+ expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"]
+
+ assert_called_with(Kernel, :system, expected_command, returns: true) do
+ with_structure_dump_flags(["--noop"]) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ end
+ end
+
def test_warn_when_external_structure_dump_command_execution_fails
filename = "awesome-file.sql"
Kernel.expects(:system)
@@ -323,6 +334,15 @@ if current_adapter?(:Mysql2Adapter)
@configuration.merge("sslca" => "ca.crt"),
filename)
end
+
+ private
+ def with_structure_dump_flags(flags)
+ old = ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags
+ yield
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = old
+ end
end
class MySQLStructureLoadTest < ActiveRecord::TestCase
@@ -335,11 +355,23 @@ if current_adapter?(:Mysql2Adapter)
def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db")
- .returns(true)
+ expected_command = ["mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db", "--noop"]
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ assert_called_with(Kernel, :system, expected_command, returns: true) do
+ with_structure_load_flags(["--noop"]) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
+ end
end
+
+ private
+ def with_structure_load_flags(flags)
+ old = ActiveRecord::Tasks::DatabaseTasks.structure_load_flags
+ ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = flags
+ yield
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = old
+ end
end
end
end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index b8c8ec88f0..512388af6b 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -74,7 +74,7 @@ if current_adapter?(:PostgreSQLAdapter)
def test_when_database_created_successfully_outputs_info_to_stdout
ActiveRecord::Tasks::DatabaseTasks.create @configuration
- assert_equal $stdout.string, "Created database 'my-app-db'\n"
+ assert_equal "Created database 'my-app-db'\n", $stdout.string
end
def test_create_when_database_exists_outputs_info_to_stderr
@@ -84,7 +84,7 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Tasks::DatabaseTasks.create @configuration
- assert_equal $stderr.string, "Database 'my-app-db' already exists\n"
+ assert_equal "Database 'my-app-db' already exists\n", $stderr.string
end
end
@@ -126,7 +126,7 @@ if current_adapter?(:PostgreSQLAdapter)
def test_when_database_dropped_successfully_outputs_info_to_stdout
ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- assert_equal $stdout.string, "Dropped database 'my-app-db'\n"
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
end
end
@@ -217,17 +217,21 @@ if current_adapter?(:PostgreSQLAdapter)
class PostgreSQLStructureDumpTest < ActiveRecord::TestCase
def setup
- @connection = stub(structure_dump: true)
+ @connection = stub(schema_search_path: nil, structure_dump: true)
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
- @filename = "awesome-file.sql"
+ @filename = "/tmp/awesome-file.sql"
+ FileUtils.touch(@filename)
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
Kernel.stubs(:system)
- File.stubs(:open)
+ end
+
+ def teardown
+ FileUtils.rm_f(@filename)
end
def test_structure_dump
@@ -236,6 +240,25 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
end
+ def test_structure_dump_header_comments_removed
+ Kernel.stubs(:system).returns(true)
+ File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n")
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+
+ assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2)
+ end
+
+ def test_structure_dump_with_extra_flags
+ expected_command = ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--noop", "my-app-db"]
+
+ assert_called_with(Kernel, :system, expected_command, returns: true) do
+ with_structure_dump_flags(["--noop"]) 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"
@@ -263,7 +286,6 @@ if current_adapter?(:PostgreSQLAdapter)
end
private
-
def with_dump_schemas(value, &block)
old_dump_schemas = ActiveRecord::Base.dump_schemas
ActiveRecord::Base.dump_schemas = value
@@ -271,6 +293,14 @@ if current_adapter?(:PostgreSQLAdapter)
ensure
ActiveRecord::Base.dump_schemas = old_dump_schemas
end
+
+ def with_structure_dump_flags(flags)
+ old = ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags
+ yield
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = old
+ end
end
class PostgreSQLStructureLoadTest < ActiveRecord::TestCase
@@ -293,12 +323,32 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
+ def test_structure_load_with_extra_flags
+ filename = "awesome-file.sql"
+ expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]]
+
+ assert_called_with(Kernel, :system, expected_command, returns: true) do
+ with_structure_load_flags(["--noop"]) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
+ end
+ end
+
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)
end
+
+ private
+ def with_structure_load_flags(flags)
+ old = ActiveRecord::Tasks::DatabaseTasks.structure_load_flags
+ ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = flags
+ yield
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = old
+ end
end
end
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 141048bfe7..0d917f3f6c 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -34,7 +34,7 @@ if current_adapter?(:SQLite3Adapter)
def test_when_db_created_successfully_outputs_info_to_stdout
ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
- assert_equal $stdout.string, "Created database '#{@database}'\n"
+ assert_equal "Created database '#{@database}'\n", $stdout.string
end
def test_db_create_when_file_exists
@@ -42,7 +42,7 @@ if current_adapter?(:SQLite3Adapter)
ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
- assert_equal $stderr.string, "Database '#{@database}' already exists\n"
+ assert_equal "Database '#{@database}' already exists\n", $stderr.string
end
def test_db_create_with_file_does_nothing
@@ -128,7 +128,7 @@ if current_adapter?(:SQLite3Adapter)
def test_when_db_dropped_successfully_outputs_info_to_stdout
ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
- assert_equal $stdout.string, "Dropped database '#{@database}'\n"
+ assert_equal "Dropped database '#{@database}'\n", $stdout.string
end
end
diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb
index 14a5faa85e..58d3bea3a2 100644
--- a/activerecord/test/cases/test_fixtures_test.rb
+++ b/activerecord/test/cases/test_fixtures_test.rb
@@ -3,28 +3,10 @@ require "cases/helper"
class TestFixturesTest < ActiveRecord::TestCase
setup do
@klass = Class.new
- @klass.send(:include, ActiveRecord::TestFixtures)
- end
-
- def test_deprecated_use_transactional_fixtures=
- assert_deprecated "use use_transactional_tests= instead" do
- @klass.use_transactional_fixtures = true
- end
- end
-
- def test_use_transactional_tests_prefers_use_transactional_fixtures
- ActiveSupport::Deprecation.silence do
- @klass.use_transactional_fixtures = false
- end
-
- assert_equal false, @klass.use_transactional_tests
+ @klass.include(ActiveRecord::TestFixtures)
end
def test_use_transactional_tests_defaults_to_true
- ActiveSupport::Deprecation.silence do
- @klass.use_transactional_fixtures = nil
- end
-
assert_equal true, @klass.use_transactional_tests
end
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 03f6c234e8..09c585167e 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -68,7 +68,7 @@ if subsecond_precision_supported?
assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output
end
- if current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter)
def test_time_precision_with_zero_should_be_dumped
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 0
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index cd83518e84..39b40e3411 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -422,7 +422,7 @@ class TimestampTest < ActiveRecord::TestCase
self.table_name = "people"
before_create do
- self.born_at = self.created_at
+ self.born_at = created_at
end
end
@@ -430,34 +430,19 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal person.born_at, nil
end
- def test_timestamp_attributes_for_create
- toy = Toy.first
- assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create)
- end
-
- def test_timestamp_attributes_for_update
- toy = Toy.first
- assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update)
- end
-
- def test_all_timestamp_attributes
- toy = Toy.first
- assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes)
- end
-
def test_timestamp_attributes_for_create_in_model
toy = Toy.first
- assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model)
+ assert_equal ["created_at"], toy.send(:timestamp_attributes_for_create_in_model)
end
def test_timestamp_attributes_for_update_in_model
toy = Toy.first
- assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model)
+ assert_equal ["updated_at"], toy.send(:timestamp_attributes_for_update_in_model)
end
def test_all_timestamp_attributes_in_model
toy = Toy.first
- assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
+ assert_equal ["created_at", "updated_at"], toy.send(:all_timestamp_attributes_in_model)
end
def test_index_is_created_for_both_timestamps
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index dba100f5c9..eaa4dd09a9 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -449,6 +449,51 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
end
end
+class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase
+ class TopicWithHistory < ActiveRecord::Base
+ self.table_name = :topics
+
+ def self.clear_history
+ @@history = []
+ end
+
+ def self.history
+ @@history ||= []
+ end
+ end
+
+ class TopicWithCallbacksOnDestroy < TopicWithHistory
+ after_commit(on: :destroy) { |record| record.class.history << :destroy }
+ end
+
+ class TopicWithCallbacksOnUpdate < TopicWithHistory
+ after_commit(on: :update) { |record| record.class.history << :update }
+ end
+
+ def test_trigger_once_on_multiple_deletions
+ TopicWithCallbacksOnDestroy.clear_history
+ topic = TopicWithCallbacksOnDestroy.new
+ topic.save
+ topic_clone = TopicWithCallbacksOnDestroy.find(topic.id)
+ topic.destroy
+ topic_clone.destroy
+
+ assert_equal [:destroy], TopicWithCallbacksOnDestroy.history
+ end
+
+ def test_trigger_on_update_where_row_was_deleted
+ TopicWithCallbacksOnUpdate.clear_history
+ topic = TopicWithCallbacksOnUpdate.new
+ topic.save
+ topic_clone = TopicWithCallbacksOnUpdate.find(topic.id)
+ topic.destroy
+ topic_clone.author_name = "Test Author"
+ topic_clone.save
+
+ assert_equal [], TopicWithCallbacksOnUpdate.history
+ end
+end
+
class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base
self.table_name = :topics
@@ -506,3 +551,43 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
assert_equal [:rollback], @topic.history
end
end
+
+class CallbacksOnActionAndConditionTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class TopicWithCallbacksOnActionAndCondition < ActiveRecord::Base
+ self.table_name = :topics
+
+ after_commit(on: [:create, :update], if: :run_callback?) { |record| record.history << :create_or_update }
+
+ def clear_history
+ @history = []
+ end
+
+ def history
+ @history ||= []
+ end
+
+ def run_callback?
+ self.history << :run_callback?
+ true
+ end
+
+ attr_accessor :save_before_commit_history, :update_title
+ end
+
+ def test_callback_on_action_with_condition
+ topic = TopicWithCallbacksOnActionAndCondition.new
+ topic.save
+ assert_equal [:run_callback?, :create_or_update], topic.history
+
+ topic.clear_history
+ topic.approved = true
+ topic.save
+ assert_equal [:run_callback?, :create_or_update], topic.history
+
+ topic.clear_history
+ topic.destroy
+ assert_equal [], topic.history
+ end
+end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 9b1cca8583..5c6d78b574 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -10,7 +10,7 @@ require "models/movie"
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_tests = false
- fixtures :topics, :developers, :authors, :posts
+ fixtures :topics, :developers, :authors, :author_addresses, :posts
def setup
@first, @second = Topic.find(1, 2).sort_by(&:id)
@@ -206,16 +206,6 @@ class TransactionTest < ActiveRecord::TestCase
assert_equal posts_count, author.posts.reload.size
end
- def test_cancellation_from_returning_false_in_before_filter
- def @first.before_save_for_transaction
- false
- end
-
- assert_deprecated do
- @first.save
- end
- end
-
def test_cancellation_from_before_destroy_rollbacks_in_destroy
add_cancelling_before_destroy_with_db_side_effect_to_topic @first
nbooks_before_destroy = Book.count
@@ -279,7 +269,11 @@ class TransactionTest < ActiveRecord::TestCase
e = assert_raises(RuntimeError) { new_topic.save }
assert_equal "Make the transaction rollback", e.message
assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value"
- assert_equal id_snapshot, new_topic.id, "The topic should have its old id"
+ if id_snapshot.nil?
+ assert_nil new_topic.id, "The topic should have its old id"
+ else
+ assert_equal id_snapshot, new_topic.id, "The topic should have its old id"
+ end
assert_equal id_present, new_topic.has_attribute?(Topic.primary_key)
end
end
diff --git a/activemodel/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb
index 026cb08a06..1cd4dbc2c5 100644
--- a/activemodel/test/cases/type/unsigned_integer_test.rb
+++ b/activerecord/test/cases/type/unsigned_integer_test.rb
@@ -1,9 +1,8 @@
require "cases/helper"
-require "active_model/type"
-module ActiveModel
+module ActiveRecord
module Type
- class UnsignedIntegerTest < ActiveModel::TestCase
+ class UnsignedIntegerTest < ActiveRecord::TestCase
test "unsigned int max value is in range" do
assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295))
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 44b4e28777..28605d2f8e 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -6,6 +6,9 @@ require "models/guid"
require "models/event"
require "models/dashboard"
require "models/uuid_item"
+require "models/author"
+require "models/person"
+require "models/essay"
class Wizard < ActiveRecord::Base
self.abstract_class = true
@@ -163,6 +166,19 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert !r2.valid?, "Saving r2 first time"
end
+ def test_validate_uniqueness_with_polymorphic_object_scope
+ Essay.validates_uniqueness_of(:name, scope: :writer)
+
+ a = Author.create(name: "Sergey")
+ p = Person.create(first_name: "Sergey")
+
+ e1 = a.essays.create(name: "Essay")
+ assert e1.valid?, "Saving e1"
+
+ e2 = p.essays.create(name: "Essay")
+ assert e2.valid?, "Saving e2"
+ end
+
def test_validate_uniqueness_with_composed_attribute_scope
r1 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world"
assert r1.valid?, "Saving r1"
@@ -356,7 +372,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
e2 = Event.create(title: "abcdefgh")
assert_not e2.valid?, "Created an event whose title is not unique"
- elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)
+ elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
assert_raise(ActiveRecord::ValueTooLong) do
Event.create(title: "abcdefgh")
end
@@ -369,13 +385,13 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_with_limit_and_utf8
if current_adapter?(:SQLite3Adapter)
- # Event.title has limit 5, but does SQLite doesn't truncate.
+ # Event.title has limit 5, but SQLite doesn't truncate.
e1 = Event.create(title: "一二三四五六七八")
assert e1.valid?, "Could not create an event with a unique 8 characters title"
e2 = Event.create(title: "一二三四五六七八")
assert_not e2.valid?, "Created an event whose title is not unique"
- elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)
+ elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
assert_raise(ActiveRecord::ValueTooLong) do
Event.create(title: "一二三四五六七八")
end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index 1f326d4b39..07288568e8 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -45,8 +45,7 @@ module ViewBehavior
def test_table_exists
view_name = Ebook.table_name
- # TODO: switch this assertion around once we changed #tables to not return views.
- ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" }
+ assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist"
end
def test_views_ara_valid_data_sources
@@ -131,8 +130,7 @@ if ActiveRecord::Base.connection.supports_views?
def test_table_exists
view_name = Paperback.table_name
- # TODO: switch this assertion around once we changed #tables to not return views.
- ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" }
+ assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist"
end
def test_column_definitions
@@ -156,7 +154,7 @@ if ActiveRecord::Base.connection.supports_views?
end
# sqlite dose not support CREATE, INSERT, and DELETE for VIEW
- if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)
class UpdateableViewTest < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :books
@@ -202,8 +200,8 @@ if ActiveRecord::Base.connection.supports_views?
end
end
end
- end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)`
-end # end fo `if ActiveRecord::Base.connection.supports_views?`
+ end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)`
+end # end of `if ActiveRecord::Base.connection.supports_views?`
if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) &&
ActiveRecord::Base.connection.supports_materialized_views?
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 5192e5050a..ab0e67cd9d 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -5,7 +5,7 @@ require "models/post"
require "models/author"
class YamlSerializationTest < ActiveRecord::TestCase
- fixtures :topics, :authors, :posts
+ fixtures :topics, :authors, :author_addresses, :posts
def test_to_yaml_with_time_with_zone_should_not_raise_exception
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
@@ -95,7 +95,7 @@ class YamlSerializationTest < ActiveRecord::TestCase
topic = YAML.load(yaml_fixture("rails_4_1"))
assert topic.new_record?
- assert_equal nil, topic.id
+ assert_nil topic.id
assert_equal "The First Topic", topic.title
assert_equal({ omg: :lol }, topic.content)
end
diff --git a/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml b/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml
new file mode 100644
index 0000000000..6f9da79b45
--- /dev/null
+++ b/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml
@@ -0,0 +1,3 @@
+one:
+ id: 1
+two: ['not a hash']
diff --git a/activerecord/test/fixtures/other_dogs.yml b/activerecord/test/fixtures/other_dogs.yml
new file mode 100644
index 0000000000..b576861929
--- /dev/null
+++ b/activerecord/test/fixtures/other_dogs.yml
@@ -0,0 +1,2 @@
+lassie:
+ id: 1
diff --git a/activerecord/test/fixtures/subscribers.yml b/activerecord/test/fixtures/subscribers.yml
index c6a8c2fa24..0f6e0cd48e 100644
--- a/activerecord/test/fixtures/subscribers.yml
+++ b/activerecord/test/fixtures/subscribers.yml
@@ -6,6 +6,6 @@ second:
nick: webster132
name: David Heinemeier Hansson
-thrid:
+third:
nick: swistak
name: Marcin Raczkowski \ No newline at end of file
diff --git a/activerecord/test/models/boolean.rb b/activerecord/test/models/boolean.rb
index 7bae22e5f9..0da228aac2 100644
--- a/activerecord/test/models/boolean.rb
+++ b/activerecord/test/models/boolean.rb
@@ -1,2 +1,5 @@
class Boolean < ActiveRecord::Base
+ def has_fun
+ super
+ end
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 4561b3132b..20e37710e7 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -216,14 +216,12 @@ class Account < ActiveRecord::Base
validate :check_empty_credit_limit
- protected
+ private
def check_empty_credit_limit
errors.add("credit_limit", :blank) if credit_limit.blank?
end
- private
-
def private_method
"Sir, yes sir!"
end
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index 682f99e365..0782c1eff4 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -88,7 +88,7 @@ module MyApplication
validate :check_empty_credit_limit
- protected
+ private
def check_empty_credit_limit
errors.add("credit_card", :blank) if credit_card.blank?
diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb
index 60af7c2247..3d40cb1ace 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -56,7 +56,7 @@ class GpsLocation
end
def ==(other)
- self.latitude == other.latitude && self.longitude == other.longitude
+ latitude == other.latitude && longitude == other.longitude
end
end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 5ca1d37f6d..654830ba11 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -251,7 +251,7 @@ class ThreadsafeDeveloper < ActiveRecord::Base
self.table_name = "developers"
def self.default_scope
- sleep 0.05 if Thread.current[:long_default_scope]
+ Thread.current[:default_scope_delay].call
limit(1)
end
end
@@ -260,3 +260,9 @@ class CachedDeveloper < ActiveRecord::Base
self.table_name = "developers"
self.cache_timestamp_format = :number
end
+
+class DeveloperWithIncorrectlyOrderedHasManyThrough < ActiveRecord::Base
+ self.table_name = "developers"
+ has_many :companies, through: :contracts
+ has_many :contracts, foreign_key: :developer_id
+end
diff --git a/activerecord/test/models/family.rb b/activerecord/test/models/family.rb
new file mode 100644
index 0000000000..5ae5a78c95
--- /dev/null
+++ b/activerecord/test/models/family.rb
@@ -0,0 +1,4 @@
+class Family < ActiveRecord::Base
+ has_many :family_trees, -> { where(token: nil) }
+ has_many :members, through: :family_trees
+end
diff --git a/activerecord/test/models/family_tree.rb b/activerecord/test/models/family_tree.rb
new file mode 100644
index 0000000000..cd9829fedd
--- /dev/null
+++ b/activerecord/test/models/family_tree.rb
@@ -0,0 +1,4 @@
+class FamilyTree < ActiveRecord::Base
+ belongs_to :member, class_name: "User", foreign_key: "member_id"
+ belongs_to :family
+end
diff --git a/activerecord/test/models/non_primary_key.rb b/activerecord/test/models/non_primary_key.rb
new file mode 100644
index 0000000000..1cafb09608
--- /dev/null
+++ b/activerecord/test/models/non_primary_key.rb
@@ -0,0 +1,2 @@
+class NonPrimaryKey < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/other_dog.rb b/activerecord/test/models/other_dog.rb
new file mode 100644
index 0000000000..418caf34be
--- /dev/null
+++ b/activerecord/test/models/other_dog.rb
@@ -0,0 +1,5 @@
+require_dependency "models/arunit2_model"
+
+class OtherDog < ARUnit2Model
+ self.table_name = "dogs"
+end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index 5b693664d4..1e5f9285a8 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -13,6 +13,11 @@ class Parrot < ActiveRecord::Base
def cancel_save_callback_method
throw(:abort)
end
+
+ before_update :increment_updated_count
+ def increment_updated_count
+ self.updated_count += 1
+ end
end
class LiveParrot < Parrot
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index e74aedb814..a2028b3eb9 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -59,6 +59,10 @@ class Post < ActiveRecord::Base
def the_association
proxy_association
end
+
+ def with_content(content)
+ self.detect { |comment| comment.body == content }
+ end
end
has_many :comments_with_extend, extend: NamedExtension, class_name: "Comment", foreign_key: "post_id" do
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index 5009f8f54b..4fbd986e40 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -11,7 +11,7 @@ class Project < ActiveRecord::Base
after_add: Proc.new { |o, r| o.developers_log << "after_adding#{r.id || '<new>'}" },
before_remove: Proc.new { |o, r| o.developers_log << "before_removing#{r.id}" },
after_remove: Proc.new { |o, r| o.developers_log << "after_removing#{r.id}" }
- has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer"
+ has_and_belongs_to_many :well_paid_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer"
belongs_to :firm
has_one :lead_developer, through: :firm, inverse_of: :contracted_projects
diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb
index 29e290825e..504f68a296 100644
--- a/activerecord/test/models/subject.rb
+++ b/activerecord/test/models/subject.rb
@@ -5,7 +5,7 @@ class Subject < ActiveRecord::Base
# as otherwise synonym test was failing
after_initialize :set_email_address
- protected
+ private
def set_email_address
unless persisted?
self.author_email_address = "test@test.com"
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index db04735d01..0420e2d15c 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -73,7 +73,7 @@ class Topic < ActiveRecord::Base
write_attribute(:approved, val)
end
- protected
+ private
def default_written_on
self.written_on = Time.now unless attribute_present?("written_on")
diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb
index 47649e0a77..5089a795f4 100644
--- a/activerecord/test/models/user.rb
+++ b/activerecord/test/models/user.rb
@@ -5,8 +5,12 @@ class User < ActiveRecord::Base
has_secure_token :auth_token
has_and_belongs_to_many :jobs_pool,
- class_name: Job,
+ class_name: "Job",
join_table: "jobs_pool"
+
+ has_one :family_tree, -> { where(token: nil) }, foreign_key: "member_id"
+ has_one :family, through: :family_tree
+ has_many :family_members, through: :family, source: :members
end
class UserWithNotification < User
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 9a203a7293..90a314c83c 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -6,6 +6,11 @@ ActiveRecord::Schema.define do
end
end
+ create_table :timestamp_defaults, force: true do |t|
+ t.timestamp :nullable_timestamp
+ t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" }
+ end
+
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index f00b858ea6..860c63b27c 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,6 +1,7 @@
ActiveRecord::Schema.define do
enable_extension!("uuid-ossp", ActiveRecord::Base.connection)
+ enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid?
create_table :uuid_parents, id: :uuid, force: true do |t|
t.string :name
@@ -22,16 +23,24 @@ ActiveRecord::Schema.define do
t.string :char2, limit: 50, default: "a varchar field"
t.text :char3, default: "a text field"
t.bigint :bigint_default, default: -> { "0::bigint" }
- t.text :multiline_default, default: '--- []
+ t.text :multiline_default, default: "--- []
-'
+"
end
- %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones
- postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
- drop_table table_name, if_exists: true
+ create_table :postgresql_times, force: true do |t|
+ t.interval :time_interval
+ t.interval :scaled_time_interval, precision: 6
end
+ create_table :postgresql_oids, force: true do |t|
+ t.oid :obj_id
+ end
+
+ drop_table "postgresql_timestamp_with_zones", if_exists: true
+ drop_table "postgresql_partitioned_table", if_exists: true
+ drop_table "postgresql_partitioned_table_parent", if_exists: true
+
execute "DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE"
execute "CREATE SEQUENCE companies_nonstd_seq START 101 OWNED BY companies.id"
execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')"
@@ -44,21 +53,6 @@ ActiveRecord::Schema.define do
end
execute <<_SQL
- CREATE TABLE postgresql_times (
- id SERIAL PRIMARY KEY,
- time_interval INTERVAL,
- scaled_time_interval INTERVAL(6)
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_oids (
- id SERIAL PRIMARY KEY,
- obj_id OID
- );
-_SQL
-
- execute <<_SQL
CREATE TABLE postgresql_timestamp_with_zones (
id SERIAL PRIMARY KEY,
time TIMESTAMP WITH TIME ZONE
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index d889f46031..08bef08abc 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -54,8 +54,8 @@ ActiveRecord::Schema.define do
create_table :authors, force: true do |t|
t.string :name, null: false
- t.integer :author_address_id
- t.integer :author_address_extra_id
+ t.references :author_address
+ t.references :author_address_extra
t.string :organization_id
t.string :owned_essay_id
end
@@ -88,7 +88,7 @@ ActiveRecord::Schema.define do
end
create_table :books, force: true do |t|
- t.integer :author_id
+ t.references :author
t.string :format
t.column :name, :string
t.column :status, :integer, default: 0
@@ -126,6 +126,9 @@ ActiveRecord::Schema.define do
t.timestamps null: false
end
+ create_table :old_cars, id: :integer, force: true do |t|
+ end
+
create_table :carriers, force: true
create_table :categories, force: true do |t|
@@ -198,7 +201,7 @@ ActiveRecord::Schema.define do
t.integer :account_id
t.string :description, default: ""
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 [: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?
end
@@ -303,7 +306,7 @@ ActiveRecord::Schema.define do
end
create_table :engines, force: true do |t|
- t.integer :car_id
+ t.references :car, index: false
end
create_table :entrants, force: true do |t|
@@ -326,6 +329,15 @@ ActiveRecord::Schema.define do
create_table :eyes, force: true do |t|
end
+ create_table :families, force: true do |t|
+ end
+
+ create_table :family_trees, force: true do |t|
+ t.references :family
+ t.references :member
+ t.string :token
+ end
+
create_table :funny_jokes, force: true do |t|
t.string :name
end
@@ -571,6 +583,7 @@ ActiveRecord::Schema.define do
t.column :color, :string
t.column :parrot_sti_class, :string
t.column :killer_id, :integer
+ t.column :updated_count, :integer, default: 0
if subsecond_precision_supported?
t.column :created_at, :datetime, precision: 0
t.column :created_on, :datetime, precision: 0
@@ -651,8 +664,8 @@ ActiveRecord::Schema.define do
end
create_table :posts, force: true do |t|
- t.integer :author_id
- t.string :title, null: false
+ t.references :author
+ t.string :title, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
@@ -1001,16 +1014,14 @@ ActiveRecord::Schema.define do
create_table :records, force: true do |t|
end
- if supports_foreign_keys?
- # fk_test_has_fk should be before fk_test_has_pk
- create_table :fk_test_has_fk, force: true do |t|
- t.integer :fk_id, null: false
+ disable_referential_integrity do
+ create_table :fk_test_has_pk, primary_key: "pk_id", force: :cascade do |t|
end
- create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t|
+ create_table :fk_test_has_fk, force: true do |t|
+ t.references :fk, null: false
+ t.foreign_key :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
end
-
- add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
end
create_table :overloaded_types, force: true do |t|
@@ -1028,6 +1039,10 @@ ActiveRecord::Schema.define do
create_table :test_with_keyword_column_name, force: true do |t|
t.string :desc
end
+
+ create_table :non_primary_keys, force: true, id: false do |t|
+ t.integer :id
+ end
end
Course.connection.create_table :courses, force: true do |t|
@@ -1047,3 +1062,5 @@ Professor.connection.create_table :courses_professors, id: false, force: true do
t.references :course
t.references :professor
end
+
+OtherDog.connection.create_table :dogs, force: true
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
deleted file mode 100644
index cc7c36fe2b..0000000000
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-ActiveRecord::Schema.define do
- execute "DROP TABLE fk_test_has_fk" rescue nil
- execute "DROP TABLE fk_test_has_pk" rescue nil
- execute <<_SQL
- CREATE TABLE 'fk_test_has_pk' (
- 'pk_id' INTEGER NOT NULL PRIMARY KEY
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE 'fk_test_has_fk' (
- 'id' INTEGER NOT NULL PRIMARY KEY,
- 'fk_id' INTEGER NOT NULL,
-
- FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id')
- );
-_SQL
-end
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index c9260398e2..1a609e13c3 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -2,6 +2,7 @@ require "active_support/logger"
require "models/college"
require "models/course"
require "models/professor"
+require "models/other_dog"
module ARTest
def self.connection_name
@@ -9,7 +10,10 @@ module ARTest
end
def self.connection_config
- config["connections"][connection_name]
+ config.fetch("connections").fetch(connection_name) do
+ puts "Connection #{connection_name.inspect} not found. Available connections: #{config['connections'].keys.join(', ')}"
+ exit 1
+ end
end
def self.connect
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 10095ee1bd..6f99155199 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,328 +1 @@
-* Remove deprecated class `ActiveSupport::Concurrency::Latch`
-
- *Andrew White*
-
-* Remove deprecated separator argument from `parameterize`
-
- *Andrew White*
-
-* Remove deprecated method `Numeric#to_formatted_s`
-
- *Andrew White*
-
-* Remove deprecated method `alias_method_chain`
-
- *Andrew White*
-
-* Remove deprecated constant `MissingSourceFile`
-
- *Andrew White*
-
-* Remove deprecated methods `Module.qualified_const_defined?`,
- `Module.qualified_const_get` and `Module.qualified_const_set`
-
- *Andrew White*
-
-* Remove deprecated `:prefix` option from `number_to_human_size`
-
- *Andrew White*
-
-* Remove deprecated method `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default`
-
- *Andrew White*
-
-* Remove deprecated file `active_support/core_ext/time/marshal.rb`
-
- *Andrew White*
-
-* Remove deprecated file `active_support/core_ext/struct.rb`
-
- *Andrew White*
-
-* Remove deprecated file `active_support/core_ext/module/method_transplanting.rb`
-
- *Andrew White*
-
-* Remove deprecated method `Module.local_constants`
-
- *Andrew White*
-
-* Remove deprecated file `active_support/core_ext/kernel/debugger.rb`
-
- *Andrew White*
-
-* Remove deprecated method `ActiveSupport::Cache::Store#namespaced_key`
-
- *Andrew White*
-
-* Remove deprecated method `ActiveSupport::Cache::Strategy::LocalCache::LocalStore#set_cache_value`
-
- *Andrew White*
-
-* Remove deprecated method `ActiveSupport::Cache::MemCacheStore#escape_key`
-
- *Andrew White*
-
-* Remove deprecated method `ActiveSupport::Cache::FileStore#key_file_path`
-
- *Andrew White*
-
-* Ensure duration parsing is consistent across DST changes
-
- Previously `ActiveSupport::Duration.parse` used `Time.current` and
- `Time#advance` to calculate the number of seconds in the duration
- from an arbitrary collection of parts. However as `advance` tries to
- be consistent across DST boundaries this meant that either the
- duration was shorter or longer depending on the time of year.
-
- This was fixed by using an absolute reference point in UTC which
- isn't subject to DST transitions. An arbitrary date of Jan 1st, 2000
- was chosen for no other reason that it seemed appropriate.
-
- Additionally, duration parsing should now be marginally faster as we
- are no longer creating instances of `ActiveSupport::TimeWithZone`
- every time we parse a duration string.
-
- Fixes #26941.
-
- *Andrew White*
-
-* Use `Hash#compact` and `Hash#compact!` from Ruby 2.4. Old Ruby versions
- will continue to get these methods from Active Support as before.
-
- *Prathamesh Sonpatki*
-
-* Fix `ActiveSupport::TimeZone#strptime`.
- Support for timestamps in format of seconds (%s) and milliseconds (%Q).
-
- Fixes #26840.
-
- *Lev Denisov*
-
-* Fix `DateAndTime::Calculations#copy_time_to`. Copy `nsec` instead of `usec`.
-
- Jumping forward or backward between weeks now preserves nanosecond digits.
-
- *Josua Schmid*
-
-* Fix `ActiveSupport::TimeWithZone#in` across DST boundaries.
-
- Previously calls to `in` were being sent to the non-DST aware
- method `Time#since` via `method_missing`. It is now aliased to
- the DST aware `ActiveSupport::TimeWithZone#+` which handles
- transitions across DST boundaries, e.g:
-
- Time.zone = "US/Eastern"
-
- t = Time.zone.local(2016,11,6,1)
- # => Sun, 06 Nov 2016 01:00:00 EDT -05:00
-
- t.in(1.hour)
- # => Sun, 06 Nov 2016 01:00:00 EST -05:00
-
- Fixes #26580.
-
- *Thomas Balthazar*
-
-* Remove unused parameter `options = nil` for `#clear` of
- `ActiveSupport::Cache::Strategy::LocalCache::LocalStore` and
- `ActiveSupport::Cache::Strategy::LocalCache`.
-
- *Yosuke Kabuto*
-
-* Fix `thread_mattr_accessor` subclass no longer overwrites parent.
-
- Assigning a value to a subclass using `thread_mattr_accessor` no
- longer changes the value of the parent class. This brings the
- behavior inline with the documentation.
-
- Given:
-
- class Account
- thread_mattr_accessor :user
- end
-
- class Customer < Account
- end
-
- Account.user = "DHH"
- Customer.user = "Rafael"
-
- Before:
-
- Account.user # => "Rafael"
-
- After:
-
- Account.user # => "DHH"
-
- *Shinichi Maeshima*
-
-* Since weeks are no longer converted to days, add `:weeks` to the list of
- parts that `ActiveSupport::TimeWithZone` will recognize as possibly being
- of variable duration to take account of DST transitions.
-
- Fixes #26039.
-
- *Andrew White*
-
-* Defines `Regexp.match?` for Ruby versions prior to 2.4. The predicate
- has the same interface, but it does not have the performance boost. Its
- purpose is to be able to write 2.4 compatible code.
-
- *Xavier Noria*
-
-* Allow `MessageEncryptor` to take advantage of authenticated encryption modes.
-
- AEAD modes like `aes-256-gcm` provide both confidentiality and data
- authenticity, eliminating the need to use `MessageVerifier` to check if the
- encrypted data has been tampered with. This speeds up encryption/decryption
- and results in shorter cipher text.
-
- *Bart de Water*
-
-* Introduce `assert_changes` and `assert_no_changes`.
-
- `assert_changes` is a more general `assert_difference` that works with any
- value.
-
- assert_changes 'Error.current', from: nil, to: 'ERR' do
- expected_bad_operation
- end
-
- Can be called with strings, to be evaluated in the binding (context) of
- the block given to the assertion, or a lambda.
-
- assert_changes -> { Error.current }, from: nil, to: 'ERR' do
- expected_bad_operation
- end
-
- The `from` and `to` arguments are compared with the case operator (`===`).
-
- assert_changes 'Error.current', from: nil, to: Error do
- expected_bad_operation
- end
-
- This is pretty useful, if you need to loosely compare a value. For example,
- you need to test a token has been generated and it has that many random
- characters.
-
- user = User.start_registration
- assert_changes 'user.token', to: /\w{32}/ do
- user.finish_registration
- end
-
- *Genadi Samokovarov*
-
-* Fix `ActiveSupport::TimeZone#strptime`. Now raises `ArgumentError` when the
- given time doesn't match the format. The error is the same as the one given
- by Ruby's `Date.strptime`. Previously it raised
- `NoMethodError: undefined method empty? for nil:NilClass.` due to a bug.
-
- Fixes #25701.
-
- *John Gesimondo*
-
-* `travel/travel_to` travel time helpers, now raise on nested calls,
- as this can lead to confusing time stubbing.
-
- Instead of:
-
- travel_to 2.days.from_now do
- # 2 days from today
- travel_to 3.days.from_now do
- # 5 days from today
- end
- end
-
- preferred way to achieve above is:
-
- travel 2.days do
- # 2 days from today
- end
-
- travel 5.days do
- # 5 days from today
- end
-
- *Vipul A M*
-
-* Support parsing JSON time in ISO8601 local time strings in
- `ActiveSupport::JSON.decode` when `parse_json_times` is enabled.
- Strings in the format of `YYYY-MM-DD hh:mm:ss` (without a `Z` at
- the end) will be parsed in the local timezone (`Time.zone`). In
- addition, date strings (`YYYY-MM-DD`) are now parsed into `Date`
- objects.
-
- *Grzegorz Witek*
-
-* Fixed `ActiveSupport::Logger.broadcast` so that calls to `#silence` now
- properly delegate to all loggers. Silencing now properly suppresses logging
- to both the log and the console.
-
- *Kevin McPhillips*
-
-* Remove deprecated arguments in `assert_nothing_raised`.
-
- *Rafel Mendonça França*
-
-* `Date.to_s` doesn't produce too many spaces. For example, `to_s(:short)`
- will now produce `01 Feb` instead of ` 1 Feb`.
-
- Fixes #25251.
-
- *Sean Griffin*
-
-* Introduce `Module#delegate_missing_to`.
-
- When building a decorator, a common pattern emerges:
-
- class Partition
- def initialize(first_event)
- @events = [ first_event ]
- end
-
- def people
- if @events.first.detail.people.any?
- @events.collect { |e| Array(e.detail.people) }.flatten.uniq
- else
- @events.collect(&:creator).uniq
- end
- end
-
- private
- def respond_to_missing?(name, include_private = false)
- @events.respond_to?(name, include_private)
- end
-
- def method_missing(method, *args, &block)
- @events.send(method, *args, &block)
- end
- end
-
- With `Module#delegate_missing_to`, the above is condensed to:
-
- class Partition
- delegate_missing_to :@events
-
- def initialize(first_event)
- @events = [ first_event ]
- end
-
- def people
- if @events.first.detail.people.any?
- @events.collect { |e| Array(e.detail.people) }.flatten.uniq
- else
- @events.collect(&:creator).uniq
- end
- end
- end
-
- *Genadi Samokovarov*, *DHH*
-
-* Rescuable: If a handler doesn't match the exception, check for handlers
- matching the exception's cause.
-
- *Jeremy Daer*
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE
index 40235833ba..6b3cead1a7 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2016 David Heinemeier Hansson
+Copyright (c) 2005-2017 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/bin/generate_tables b/activesupport/bin/generate_tables
index 5d912f375c..aa36a01b5b 100755
--- a/activesupport/bin/generate_tables
+++ b/activesupport/bin/generate_tables
@@ -8,6 +8,7 @@ end
require "open-uri"
require "tmpdir"
+require "fileutils"
module ActiveSupport
module Multibyte
@@ -101,9 +102,10 @@ module ActiveSupport
def parse
SOURCES.each do |type, url|
- filename = File.join(Dir.tmpdir, "#{url.split('/').last}")
+ 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 }
diff --git a/activesupport/bin/test b/activesupport/bin/test
index 84a05bba08..a7beb14b27 100755
--- a/activesupport/bin/test
+++ b/activesupport/bin/test
@@ -2,5 +2,3 @@
COMPONENT_ROOT = File.expand_path("..", __dir__)
require File.expand_path("../tools/test", COMPONENT_ROOT)
-
-exit Minitest.run(ARGV)
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 2fc42e1975..03e3ce821a 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2005-2016 David Heinemeier Hansson
+# Copyright (c) 2005-2017 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -80,11 +80,15 @@ module ActiveSupport
cattr_accessor :test_order # :nodoc:
def self.halt_callback_chains_on_return_false
- Callbacks.halt_and_display_warning_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)
- Callbacks.halt_and_display_warning_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
diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb
index 85122e39b2..befa1746c6 100644
--- a/activesupport/lib/active_support/array_inquirer.rb
+++ b/activesupport/lib/active_support/array_inquirer.rb
@@ -20,7 +20,7 @@ module ActiveSupport
# variants.any?(:phone, :tablet) # => true
# variants.any?('phone', 'desktop') # => true
# variants.any?(:desktop, :watch) # => false
- def any?(*candidates, &block)
+ def any?(*candidates)
if candidates.none?
super
else
@@ -32,7 +32,7 @@ module ActiveSupport
private
def respond_to_missing?(name, include_private = false)
- name[-1] == "?"
+ (name[-1] == "?") || super
end
def method_missing(name, *args)
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index 169a58ecd1..e47c90597f 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -12,7 +12,7 @@ module ActiveSupport
# is to exclude the output of a noisy library from the backtrace, so that you
# can focus on the rest.
#
- # bc = BacktraceCleaner.new
+ # bc = ActiveSupport::BacktraceCleaner.new
# bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
# bc.add_silencer { |line| line =~ /puma|rubygems/ } # skip any lines from puma or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 8f0a954882..4d8c2046e8 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -6,7 +6,6 @@ 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"
-require "active_support/core_ext/string/strip"
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
@@ -71,8 +70,8 @@ module ActiveSupport
# each of elements in the array will be turned into parameters/keys and
# concatenated into a single key. For example:
#
- # expand_cache_key([:foo, :bar]) # => "foo/bar"
- # expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar"
+ # ActiveSupport::Cache.expand_cache_key([:foo, :bar]) # => "foo/bar"
+ # ActiveSupport::Cache.expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar"
#
# The +key+ argument can also respond to +cache_key+ or +to_param+.
def expand_cache_key(key, namespace = nil)
@@ -471,12 +470,12 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support clear")
end
- protected
+ private
# Adds the namespace defined in the options to a pattern designed to
# match keys. Implementations that support delete_matched should call
# this method to translate a pattern that matches names into one that
# matches namespaced keys.
- def key_matcher(pattern, options)
+ def key_matcher(pattern, options) # :doc:
prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
if prefix
source = pattern.source
@@ -493,25 +492,24 @@ module ActiveSupport
# Reads an entry from the cache implementation. Subclasses must implement
# this method.
- def read_entry(key, options) # :nodoc:
+ def read_entry(key, options)
raise NotImplementedError.new
end
# Writes an entry to the cache implementation. Subclasses must implement
# this method.
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
raise NotImplementedError.new
end
# Deletes an entry from the cache implementation. Subclasses must
# implement this method.
- def delete_entry(key, options) # :nodoc:
+ def delete_entry(key, options)
raise NotImplementedError.new
end
- private
# Merges the default options with ones specific to a method call.
- def merged_options(call_options) # :nodoc:
+ def merged_options(call_options)
if call_options
options.merge(call_options)
else
@@ -522,7 +520,7 @@ module ActiveSupport
# Expands key to be a consistent string value. Invokes +cache_key+ if
# object responds to +cache_key+. Otherwise, +to_param+ method will be
# called. If the key is a Hash, then keys will be sorted alphabetically.
- def expanded_key(key) # :nodoc:
+ def expanded_key(key)
return key.cache_key.to_s if key.respond_to?(:cache_key)
case key
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 34456a594f..d5c8585816 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -66,7 +66,7 @@ module ActiveSupport
end
end
- protected
+ private
def read_entry(key, options)
if File.exist?(key)
@@ -98,9 +98,8 @@ module ActiveSupport
end
end
- private
# Lock a file for a block so only one process can modify it at a time.
- def lock_file(file_name, &block) # :nodoc:
+ def lock_file(file_name, &block)
if File.exist?(file_name)
File.open(file_name, "r+") do |f|
begin
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index e99d65bdf2..e09cee3335 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -26,7 +26,7 @@ module ActiveSupport
class MemCacheStore < Store
# Provide support for raw values in the local cache strategy.
module LocalCacheWithRaw # :nodoc:
- protected
+ private
def read_entry(key, options)
entry = super
if options[:raw] && local_cache && entry
@@ -35,7 +35,7 @@ module ActiveSupport
entry
end
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
if options[:raw] && local_cache
raw_entry = Entry.new(entry.value.to_s)
raw_entry.expires_at = entry.expires_at
@@ -143,14 +143,14 @@ module ActiveSupport
@data.stats
end
- protected
+ private
# Read an entry from the cache.
- def read_entry(key, options) # :nodoc:
+ def read_entry(key, options)
rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) }
end
# Write an entry to the cache.
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
method = options && options[:unless_exist] ? :add : :set
value = options[:raw] ? entry.value.to_s : entry
expires_in = options[:expires_in].to_i
@@ -164,12 +164,10 @@ module ActiveSupport
end
# Delete an entry from the cache.
- def delete_entry(key, options) # :nodoc:
+ def delete_entry(key, options)
rescue_error_with(false) { @data.delete(key) }
end
- private
-
# Memcache keys are binaries. So we need to force their encoding to binary
# before applying the regular expression to ensure we are escaping all
# characters properly.
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 968e72d765..56fe1457d0 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -28,6 +28,7 @@ module ActiveSupport
@pruning = false
end
+ # Delete all data stored in a given cache store.
def clear(options = nil)
synchronize do
@data.clear
@@ -83,6 +84,7 @@ module ActiveSupport
modify_value(name, -amount, options)
end
+ # Deletes cache entries if the cache key matches a given pattern.
def delete_matched(matcher, options = nil)
options = merged_options(options)
instrument(:delete_matched, matcher.inspect) do
@@ -104,15 +106,15 @@ module ActiveSupport
@monitor.synchronize(&block)
end
- protected
+ private
PER_ENTRY_OVERHEAD = 240
- def cached_size(key, entry) # :nodoc:
+ def cached_size(key, entry)
key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
end
- def read_entry(key, options) # :nodoc:
+ def read_entry(key, options)
entry = @data[key]
synchronize do
if entry
@@ -124,7 +126,7 @@ module ActiveSupport
entry
end
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
entry.dup_value!
synchronize do
old_entry = @data[key]
@@ -141,7 +143,7 @@ module ActiveSupport
end
end
- def delete_entry(key, options) # :nodoc:
+ def delete_entry(key, options)
synchronize do
@key_access.delete(key)
entry = @data.delete(key)
@@ -150,8 +152,6 @@ module ActiveSupport
end
end
- private
-
def modify_value(name, amount, options)
synchronize do
options = merged_options(options)
diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb
index 0564ce5312..550659fc56 100644
--- a/activesupport/lib/active_support/cache/null_store.rb
+++ b/activesupport/lib/active_support/cache/null_store.rb
@@ -25,15 +25,15 @@ module ActiveSupport
def delete_matched(matcher, options = nil)
end
- protected
- def read_entry(key, options) # :nodoc:
+ private
+ def read_entry(key, options)
end
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
true
end
- def delete_entry(key, options) # :nodoc:
+ def delete_entry(key, options)
false
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 7652cae201..672eb2bb80 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -105,8 +105,8 @@ module ActiveSupport
value
end
- protected
- def read_entry(key, options) # :nodoc:
+ private
+ def read_entry(key, options)
if cache = local_cache
cache.fetch_entry(key) { super }
else
@@ -114,17 +114,17 @@ module ActiveSupport
end
end
- def write_entry(key, entry, options) # :nodoc:
+ def write_entry(key, entry, options)
local_cache.write_entry(key, entry, options) if local_cache
super
end
- def delete_entry(key, options) # :nodoc:
+ def delete_entry(key, options)
local_cache.delete_entry(key, options) if local_cache
super
end
- def write_cache_value(name, value, options) # :nodoc:
+ def write_cache_value(name, value, options)
name = normalize_key(name, options)
cache = local_cache
cache.mute do
@@ -136,8 +136,6 @@ module ActiveSupport
end
end
- private
-
def local_cache_key
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
index 174cb72b1e..4c3679e4bf 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
@@ -28,13 +28,13 @@ module ActiveSupport
response[2] = ::Rack::BodyProxy.new(response[2]) do
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
end
+ cleanup_on_body_close = true
response
rescue Rack::Utils::InvalidParameterError
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
[400, {}, []]
- rescue Exception
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
- raise
+ ensure
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
+ cleanup_on_body_close
end
end
end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index af8ddb176f..ea71569ca8 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -4,7 +4,6 @@ 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/module/attribute_accessors"
require "active_support/core_ext/string/filters"
require "active_support/deprecation"
require "thread"
@@ -69,12 +68,6 @@ module ActiveSupport
CALLBACK_FILTER_TYPES = [:before, :after, :around]
- # If true, Active Record and Active Model callbacks returning +false+ will
- # halt the entire callback chain and display a deprecation message.
- # If false, callback chains will only be halted by calling +throw :abort+.
- # Defaults to +true+.
- mattr_accessor(:halt_and_display_warning_on_return_false, instance_writer: false) { true }
-
# Runs the callbacks for the given event.
#
# Calls the before and around callbacks in the order they were set, yields
@@ -109,16 +102,22 @@ module ActiveSupport
invoke_sequence = Proc.new do
skipped = nil
while true
- current, next_sequence = next_sequence, next_sequence.nested
+ current = next_sequence
current.invoke_before(env)
if current.final?
env.value = !env.halted && (!block_given? || yield)
elsif current.skip?(env)
(skipped ||= []) << current
+ next_sequence = next_sequence.nested
next
else
- target, block, method, *arguments = current.expand_call_template(env, invoke_sequence)
- target.send(method, *arguments, &block)
+ next_sequence = next_sequence.nested
+ begin
+ target, block, method, *arguments = current.expand_call_template(env, invoke_sequence)
+ target.send(method, *arguments, &block)
+ ensure
+ next_sequence = current
+ end
end
current.invoke_after(env)
skipped.pop.invoke_after(env) while skipped && skipped.first
@@ -280,9 +279,9 @@ module ActiveSupport
class Callback #:nodoc:#
def self.build(chain, filter, kind, options)
if filter.is_a?(String)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing string to define callback is deprecated and will be removed
- in Rails 5.1 without replacement.
+ raise ArgumentError, <<-MSG.squish
+ Passing string to define a callback is not supported. See the `.set_callback`
+ documentation to see supported values.
MSG
end
@@ -637,9 +636,8 @@ module ActiveSupport
# set_callback :save, :before_method
#
# The callback can be specified as a symbol naming an instance method; as a
- # proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an
- # object that responds to a certain method determined by the <tt>:scope</tt>
- # argument to +define_callbacks+.
+ # proc, lambda, or block; or as an object that responds to a certain method
+ # determined by the <tt>:scope</tt> argument to +define_callbacks+.
#
# If a proc, lambda, or block is given, its body is evaluated in the context
# of the current object. It can also optionally accept the current object as
@@ -653,16 +651,24 @@ module ActiveSupport
#
# ===== Options
#
- # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings,
+ # * <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 or an array of symbols and
- # strings, each naming an instance method or a proc; the callback will
- # be called only when they all return a false 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>: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 :if and :unless conditional options is deprecated
+ and will be removed in Rails 5.2 without replacement.
+ MSG
+ end
+
self_chain = get_callbacks name
mapped = filters.map do |filter|
Callback.build(self_chain, filter, type, options)
@@ -686,6 +692,14 @@ module ActiveSupport
# already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>).
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|
@@ -835,30 +849,6 @@ module ActiveSupport
def set_callbacks(name, callbacks) # :nodoc:
self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
end
-
- def deprecated_false_terminator # :nodoc:
- Proc.new do |target, result_lambda|
- terminate = true
- catch(:abort) do
- result = result_lambda.call if result_lambda.is_a?(Proc)
- if Callbacks.halt_and_display_warning_on_return_false && result == false
- display_deprecation_warning_for_false_terminator
- else
- terminate = false
- end
- end
- terminate
- end
- end
-
- private
-
- def display_deprecation_warning_for_false_terminator
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1.
- To explicitly halt the callback chain, please use `throw :abort` instead.
- MSG
- end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index 52706c3d7a..f397f658f3 100644
--- a/activesupport/lib/active_support/core_ext.rb
+++ b/activesupport/lib/active_support/core_ext.rb
@@ -1,4 +1,3 @@
-DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"]
-(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path|
+(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"]).each do |path|
require path
end
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 37d833887a..fca33c9d69 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -31,7 +31,7 @@ class Array
#
# people = ["David", "Rafael", "Aaron", "Todd"]
# people.without "Aaron", "Todd"
- # => ["David", "Rafael"]
+ # # => ["David", "Rafael"]
#
# Note: This is an optimization of `Enumerable#without` that uses `Array#-`
# instead of `Array#reject` for performance reasons.
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index ea9d85f6e3..0d798e5c4e 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -89,7 +89,7 @@ class Array
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
def split(value = nil)
- arr = self.dup
+ arr = dup
result = []
if block_given?
while (idx = arr.index { |i| yield i })
diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb
index 10a7c787f6..62397d9508 100644
--- a/activesupport/lib/active_support/core_ext/class/subclasses.rb
+++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb
@@ -22,6 +22,7 @@ class Class
def descendants
descendants = []
ObjectSpace.each_object(singleton_class) do |k|
+ next if k.singleton_class?
descendants.unshift k unless k == self
end
descendants
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index 2e64097933..d6f60cac04 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -133,7 +133,7 @@ class Date
# Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there.
def compare_with_coercion(other)
if other.is_a?(Time)
- self.to_datetime <=> other
+ to_datetime <=> other
else
compare_without_coercion(other)
end
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 db95ae0db5..ab80392460 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
@@ -10,13 +10,5 @@ module DateAndTime
# this behavior, but new apps will have an initializer that sets
# this to true, because the new behavior is preferred.
mattr_accessor(:preserve_timezone, instance_writer: false) { false }
-
- def to_time
- if preserve_timezone
- @_to_time_with_instance_offset ||= getlocal(utc_offset)
- else
- @_to_time_with_system_offset ||= getlocal
- end
- end
end
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 70d5c9af8e..7a9eb8c266 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -47,13 +47,23 @@ class DateTime
# DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => DateTime.new(1981, 8, 1, 22, 35, 0)
# DateTime.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => DateTime.new(1981, 8, 29, 0, 0, 0)
def change(options)
+ if new_nsec = options[:nsec]
+ raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
+ new_fraction = Rational(new_nsec, 1000000000)
+ else
+ new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+ new_fraction = Rational(new_usec, 1000000)
+ end
+
+ raise ArgumentError, "argument out of range" if new_fraction >= 1
+
::DateTime.civil(
options.fetch(:year, year),
options.fetch(:month, month),
options.fetch(:day, day),
options.fetch(:hour, hour),
options.fetch(:min, options[:hour] ? 0 : min),
- options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction),
+ options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_fraction,
options.fetch(:offset, offset),
options.fetch(:start, start)
)
@@ -122,7 +132,7 @@ class DateTime
# Returns a new DateTime representing the end of the day (23:59:59).
def end_of_day
- change(hour: 23, min: 59, sec: 59)
+ change(hour: 23, min: 59, sec: 59, usec: Rational(999999999, 1000))
end
alias :at_end_of_day :end_of_day
@@ -134,7 +144,7 @@ class DateTime
# Returns a new DateTime representing the end of the hour (hh:59:59).
def end_of_hour
- change(min: 59, sec: 59)
+ change(min: 59, sec: 59, usec: Rational(999999999, 1000))
end
alias :at_end_of_hour :end_of_hour
@@ -146,7 +156,7 @@ class DateTime
# Returns a new DateTime representing the end of the minute (hh:mm:59).
def end_of_minute
- change(sec: 59)
+ change(sec: 59, usec: Rational(999999999, 1000))
end
alias :at_end_of_minute :end_of_minute
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 30bb7f4a60..eb8b8b2c65 100644
--- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb
@@ -1,5 +1,15 @@
require "active_support/core_ext/date_and_time/compatibility"
class DateTime
- prepend DateAndTime::Compatibility
+ include DateAndTime::Compatibility
+
+ remove_possible_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
+ # on the setting of +ActiveSupport.to_time_preserves_timezone+.
+ def to_time
+ preserve_timezone ? getlocal(utc_offset) : getlocal
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 8ebe758078..90d7d2947f 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -12,7 +12,7 @@ module Enumerable
#
# [5, 15, 10].sum # => 30
# ['foo', 'bar'].sum # => "foobar"
- # [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5]
+ # [[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:
#
@@ -29,9 +29,9 @@ module Enumerable
# Convert an enumerable to a hash.
#
# people.index_by(&:login)
- # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
+ # # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
# people.index_by { |person| "#{person.first_name} #{person.last_name}" }
- # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
+ # # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
def index_by
if block_given?
result = {}
@@ -67,10 +67,10 @@ module Enumerable
# Returns a copy of the enumerable without the specified elements.
#
# ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd"
- # => ["David", "Rafael"]
+ # # => ["David", "Rafael"]
#
# {foo: 1, bar: 2, baz: 3}.without :bar
- # => {foo: 1, baz: 3}
+ # # => {foo: 1, baz: 3}
def without(*elements)
reject { |element| elements.include?(element) }
end
@@ -78,10 +78,10 @@ module Enumerable
# Convert an enumerable to an array based on the given key.
#
# [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
- # => ["David", "Rafael", "Aaron"]
+ # # => ["David", "Rafael", "Aaron"]
#
# [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
- # => [[1, "David"], [2, "Rafael"]]
+ # # => [[1, "David"], [2, "Rafael"]]
def pluck(*keys)
if keys.many?
map { |element| keys.map { |key| element[key] } }
@@ -115,9 +115,14 @@ end
# 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)
- class Array
- alias :orig_sum :sum
+ # 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
diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb
index 4a64872392..74baae3639 100644
--- a/activesupport/lib/active_support/core_ext/integer/time.rb
+++ b/activesupport/lib/active_support/core_ext/integer/time.rb
@@ -18,12 +18,12 @@ class Integer
# # equivalent to Time.now.advance(months: 4, years: 5)
# (4.months + 5.years).from_now
def months
- ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
+ ActiveSupport::Duration.months(self)
end
alias :month :months
def years
- ActiveSupport::Duration.new(self * 365.25.days.to_i, [[:years, self]])
+ ActiveSupport::Duration.years(self)
end
alias :year :years
end
diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb
index 3cf7ea0a97..d273487010 100644
--- a/activesupport/lib/active_support/core_ext/load_error.rb
+++ b/activesupport/lib/active_support/core_ext/load_error.rb
@@ -1,6 +1,3 @@
-require "active_support/deprecation"
-require "active_support/deprecation/proxy_wrappers"
-
class LoadError
REGEXPS = [
/^no such file to load -- (.+)$/i,
@@ -9,18 +6,6 @@ class LoadError
/^cannot load such file -- (.+)$/i,
]
- unless method_defined?(:path)
- # Returns the path which was unable to be loaded.
- def path
- @path ||= begin
- REGEXPS.find do |regex|
- message =~ regex
- end
- $1
- end
- end
- end
-
# Returns true if the given path name (except perhaps for the ".rb"
# extension) is the missing file which caused the exception to be raised.
def is_missing?(location)
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
index edfc8296fe..bba2b3be2e 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -1,7 +1,7 @@
module ActiveSupport
module MarshalWithAutoloading # :nodoc:
- def load(source)
- super(source)
+ def load(source, proc = nil)
+ super(source, proc)
rescue ArgumentError, NameError => exc
if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
# try loading the class/module
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 90989f4f3d..2c24081eb9 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -7,7 +7,8 @@ require "active_support/core_ext/regexp"
class Module
# Defines a class attribute and creates a class and instance reader methods.
# The underlying class variable is set to +nil+, if it is not previously
- # defined.
+ # defined. All class and instance methods created will be public, even if
+ # this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_reader :hair_colors
@@ -76,7 +77,9 @@ class Module
alias :cattr_reader :mattr_reader
# Defines a class attribute and creates a class and instance writer methods to
- # allow assignment to the attribute.
+ # allow assignment to the attribute. All class and instance methods created
+ # will be public, even if this method is called with a private or protected
+ # access modifier.
#
# module HairColors
# mattr_writer :hair_colors
@@ -142,6 +145,8 @@ class Module
alias :cattr_writer :mattr_writer
# Defines both class and instance accessors for class attributes.
+ # All class and instance methods created will be public, even if
+ # this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_accessor :hair_colors
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index f5f4ba61b7..cdf27f49ad 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -6,11 +6,12 @@ class Module
# option is not used.
class DelegationError < NoMethodError; end
+ RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
+ else elsif END end ensure false for if in module next nil not or redo rescue retry
+ return self super then true undef unless until when while yield)
+ DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
- %w(_ arg args alias and BEGIN begin block break case class def defined? do
- else elsif END end ensure false for if in module next nil not or redo
- rescue retry return self super then true undef unless until when while
- yield)
+ RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
# Provides a +delegate+ class method to easily expose contained objects'
@@ -19,7 +20,8 @@ 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 +NoMethodError+ from being raised
+ # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+
+ # from being raised
#
# 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
@@ -111,18 +113,19 @@ class Module
# invoice.customer_address # => 'Vimmersvej 13'
#
# If the target is +nil+ and does not respond to the delegated method a
- # +NoMethodError+ 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, 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.
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :age, to: :profile
# end
#
- # User.new.age # raises NoMethodError: undefined method `age'
+ # User.new.age
+ # # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
#
# But if not having a profile yet is fine and should not be an error
# condition:
@@ -257,7 +260,7 @@ class Module
# end
#
# The target can be anything callable within the object. E.g. instance
- # variables, methods, constants ant the likes.
+ # variables, methods, constants and the likes.
def delegate_missing_to(target)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index 0665aa88bc..ca20a6d4c5 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -5,10 +5,12 @@ class Module
#
# M::N.parent_name # => "M"
def parent_name
- if defined? @parent_name
+ if defined?(@parent_name)
@parent_name
else
- @parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
+ parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
+ @parent_name = parent_name unless frozen?
+ parent_name
end
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 946f8ddeab..4f6621693e 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -99,31 +99,30 @@ module ActiveSupport::NumericWithFormat
# 1234567.to_s(:human, precision: 1,
# separator: ',',
# significant: false) # => "1,2 Million"
- def to_s(*args)
- format, options = args
- options ||= {}
-
+ def to_s(format = nil, options = nil)
case format
+ when nil
+ super()
+ when Integer, String
+ super(format)
when :phone
- return ActiveSupport::NumberHelper.number_to_phone(self, options)
+ return ActiveSupport::NumberHelper.number_to_phone(self, options || {})
when :currency
- return ActiveSupport::NumberHelper.number_to_currency(self, options)
+ return ActiveSupport::NumberHelper.number_to_currency(self, options || {})
when :percentage
- return ActiveSupport::NumberHelper.number_to_percentage(self, options)
+ return ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
when :delimited
- return ActiveSupport::NumberHelper.number_to_delimited(self, options)
+ return ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
when :rounded
- return ActiveSupport::NumberHelper.number_to_rounded(self, options)
+ return ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
when :human
- return ActiveSupport::NumberHelper.number_to_human(self, options)
+ return ActiveSupport::NumberHelper.number_to_human(self, options || {})
when :human_size
- return ActiveSupport::NumberHelper.number_to_human_size(self, options)
+ return ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
+ when Symbol
+ super()
else
- if is_a?(Float) || format.is_a?(Symbol)
- super()
- else
- super
- end
+ super(format)
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 809dfd4e07..2e6c70d418 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -19,7 +19,7 @@ class Numeric
# # equivalent to Time.current.advance(months: 4, years: 5)
# (4.months + 5.years).from_now
def seconds
- ActiveSupport::Duration.new(self, [[:seconds, self]])
+ ActiveSupport::Duration.seconds(self)
end
alias :second :seconds
@@ -27,7 +27,7 @@ class Numeric
#
# 2.minutes # => 2 minutes
def minutes
- ActiveSupport::Duration.new(self * 60, [[:minutes, self]])
+ ActiveSupport::Duration.minutes(self)
end
alias :minute :minutes
@@ -35,7 +35,7 @@ class Numeric
#
# 2.hours # => 2 hours
def hours
- ActiveSupport::Duration.new(self * 3600, [[:hours, self]])
+ ActiveSupport::Duration.hours(self)
end
alias :hour :hours
@@ -43,7 +43,7 @@ class Numeric
#
# 2.days # => 2 days
def days
- ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
+ ActiveSupport::Duration.days(self)
end
alias :day :days
@@ -51,7 +51,7 @@ class Numeric
#
# 2.weeks # => 2 weeks
def weeks
- ActiveSupport::Duration.new(self * 7.days, [[:weeks, self]])
+ ActiveSupport::Duration.weeks(self)
end
alias :week :weeks
@@ -59,7 +59,7 @@ class Numeric
#
# 2.fortnights # => 4 weeks
def fortnights
- ActiveSupport::Duration.new(self * 2.weeks, [[:weeks, self * 2]])
+ ActiveSupport::Duration.weeks(self * 2)
end
alias :fortnight :fortnights
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index aa2282cb7e..b028df97ee 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -1,7 +1,7 @@
#--
-# Most objects are cloneable, but not all. For example you can't dup +nil+:
+# Most objects are cloneable, but not all. For example you can't dup methods:
#
-# nil.dup # => TypeError: can't dup NilClass
+# method(:puts).dup # => TypeError: allocator undefined for Method
#
# Classes may signal their instances are not duplicable removing +dup+/+clone+
# or raising exceptions from them. So, to dup an arbitrary object you normally
@@ -19,7 +19,7 @@
class Object
# Can you safely dup this object?
#
- # False for +nil+, +false+, +true+, symbol, number, method objects;
+ # False for method objects;
# true otherwise.
def duplicable?
true
@@ -27,52 +27,78 @@ class Object
end
class NilClass
- # +nil+ is not duplicable:
- #
- # nil.duplicable? # => false
- # nil.dup # => TypeError: can't dup NilClass
- def duplicable?
- false
+ begin
+ nil.dup
+ rescue TypeError
+
+ # +nil+ is not duplicable:
+ #
+ # nil.duplicable? # => false
+ # nil.dup # => TypeError: can't dup NilClass
+ def duplicable?
+ false
+ end
end
end
class FalseClass
- # +false+ is not duplicable:
- #
- # false.duplicable? # => false
- # false.dup # => TypeError: can't dup FalseClass
- def duplicable?
- false
+ begin
+ false.dup
+ rescue TypeError
+
+ # +false+ is not duplicable:
+ #
+ # false.duplicable? # => false
+ # false.dup # => TypeError: can't dup FalseClass
+ def duplicable?
+ false
+ end
end
end
class TrueClass
- # +true+ is not duplicable:
- #
- # true.duplicable? # => false
- # true.dup # => TypeError: can't dup TrueClass
- def duplicable?
- false
+ begin
+ true.dup
+ rescue TypeError
+
+ # +true+ is not duplicable:
+ #
+ # true.duplicable? # => false
+ # true.dup # => TypeError: can't dup TrueClass
+ def duplicable?
+ false
+ end
end
end
class Symbol
- # Symbols are not duplicable:
- #
- # :my_symbol.duplicable? # => false
- # :my_symbol.dup # => TypeError: can't dup Symbol
- def duplicable?
- false
+ begin
+ :symbol.dup # Ruby 2.4.x.
+ "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
+ rescue TypeError
+
+ # Symbols are not duplicable:
+ #
+ # :my_symbol.duplicable? # => false
+ # :my_symbol.dup # => TypeError: can't dup Symbol
+ def duplicable?
+ false
+ end
end
end
class Numeric
- # Numbers are not duplicable:
- #
- # 3.duplicable? # => false
- # 3.dup # => TypeError: can't dup Integer
- def duplicable?
- false
+ begin
+ 1.dup
+ rescue TypeError
+
+ # Numbers are not duplicable:
+ #
+ # 3.duplicable? # => false
+ # 3.dup # => TypeError: can't dup Integer
+ def duplicable?
+ false
+ end
end
end
@@ -80,8 +106,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.new("1.2").duplicable? # => true
+ # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
def duplicable?
true
end
@@ -96,3 +122,33 @@ class Method
false
end
end
+
+class Complex
+ begin
+ Complex(1).dup
+ rescue TypeError
+
+ # Complexes are not duplicable:
+ #
+ # Complex(1).duplicable? # => false
+ # Complex(1).dup # => TypeError: can't copy Complex
+ def duplicable?
+ false
+ end
+ end
+end
+
+class Rational
+ begin
+ Rational(1).dup
+ rescue TypeError
+
+ # Rationals are not duplicable:
+ #
+ # Rational(1).duplicable? # => false
+ # Rational(1).dup # => TypeError: can't copy Rational
+ def duplicable?
+ false
+ end
+ end
+end
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 cf39b1d312..3a44e08630 100644
--- a/activesupport/lib/active_support/core_ext/object/with_options.rb
+++ b/activesupport/lib/active_support/core_ext/object/with_options.rb
@@ -62,6 +62,17 @@ class Object
#
# 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:
+ #
+ # class Phone < ActiveRecord::Base
+ # enum phone_number_type: [home: 0, office: 1, mobile: 2]
+ #
+ # with_options presence: true do
+ # validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys }
+ # end
+ # end
+ #
def with_options(options, &block)
option_merger = ActiveSupport::OptionMerger.new(self, options)
block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index b87d4235e6..539e874368 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -133,10 +133,10 @@ class String
# Removes the module part from the constant expression in the string.
#
- # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
- # 'Inflections'.demodulize # => "Inflections"
- # '::Inflections'.demodulize # => "Inflections"
- # ''.demodulize # => ''
+ # 'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections"
+ # 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ''
#
# See also +deconstantize+.
def demodulize
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 227c34b032..94ce3f6a61 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -152,18 +152,16 @@ module ActiveSupport #:nodoc:
def [](*args)
if args.size < 2
super
- else
- if html_safe?
- new_safe_buffer = super
-
- if new_safe_buffer
- new_safe_buffer.instance_variable_set :@html_safe, true
- end
+ elsif html_safe?
+ new_safe_buffer = super
- new_safe_buffer
- else
- to_str[*args]
+ if new_safe_buffer
+ new_safe_buffer.instance_variable_set :@html_safe, true
end
+
+ new_safe_buffer
+ else
+ to_str[*args]
end
end
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index cbdcb86d6d..7b7aeef25a 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -53,6 +53,29 @@ class Time
end
alias_method :at_without_coercion, :at
alias_method :at, :at_with_coercion
+
+ # Creates a +Time+ instance from an RFC 3339 string.
+ #
+ # Time.rfc3339('1999-12-31T14:00:00-10:00') # => 2000-01-01 00:00:00 -1000
+ #
+ # If the time or offset components are missing then an +ArgumentError+ will be raised.
+ #
+ # Time.rfc3339('1999-12-31') # => ArgumentError: invalid date
+ def rfc3339(str)
+ parts = Date._rfc3339(str)
+
+ raise ArgumentError, "invalid date" if parts.empty?
+
+ Time.new(
+ parts.fetch(:year),
+ parts.fetch(:mon),
+ parts.fetch(:mday),
+ parts.fetch(:hour),
+ parts.fetch(:min),
+ parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset)
+ )
+ end
end
# Returns the number of seconds since 00:00:00.
diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb
index ca4b9574d5..45e86b77ce 100644
--- a/activesupport/lib/active_support/core_ext/time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb
@@ -1,5 +1,14 @@
require "active_support/core_ext/date_and_time/compatibility"
+require "active_support/core_ext/module/remove_method"
class Time
- prepend DateAndTime::Compatibility
+ include DateAndTime::Compatibility
+
+ remove_possible_method :to_time
+
+ # Either return +self+ or the time in the local system timezone depending
+ # on the setting of +ActiveSupport.to_time_preserves_timezone+.
+ def to_time
+ preserve_timezone ? self : getlocal
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index f2bbe55aa6..595bda6b4f 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -64,4 +64,7 @@ class Time
def formatted_offset(colon = true, alternate_utc_string = nil)
utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
+
+ # Aliased to +xmlschema+ for compatibility with +DateTime+
+ alias_method :rfc3339, :xmlschema
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 0146401abe..e125b657f2 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -242,7 +242,7 @@ module ActiveSupport #:nodoc:
# resolution deterministic for constants with the same relative name in
# different namespaces whose evaluation would depend on load order
# otherwise.
- def require_dependency(file_name, message = "No such file to load -- %s")
+ def require_dependency(file_name, message = "No such file to load -- %s.rb")
file_name = file_name.to_path if file_name.respond_to?(:to_path)
unless file_name.is_a?(String)
raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 191e582de8..72c74e966a 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -15,6 +15,7 @@ module ActiveSupport
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"
@@ -32,7 +33,7 @@ module ActiveSupport
# and the second is a library name
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- def initialize(deprecation_horizon = "5.2", gem_name = "Rails")
+ def initialize(deprecation_horizon = "5.3", 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/constant_accessor.rb b/activesupport/lib/active_support/deprecation/constant_accessor.rb
new file mode 100644
index 0000000000..2b19de365f
--- /dev/null
+++ b/activesupport/lib/active_support/deprecation/constant_accessor.rb
@@ -0,0 +1,50 @@
+require "active_support/inflector/methods"
+
+module ActiveSupport
+ class Deprecation
+ # DeprecatedConstantAccessor transforms a constant into a deprecated one by
+ # hooking +const_missing+.
+ #
+ # It takes the names of an old (deprecated) constant and of a new constant
+ # (both in string form) and optionally a deprecator. The deprecator defaults
+ # to +ActiveSupport::Deprecator+ if none is specified.
+ #
+ # The deprecated constant now returns the same object as the new one rather
+ # than a proxy object, so it can be used transparently in +rescue+ blocks
+ # etc.
+ #
+ # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
+ #
+ # (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
+ # deprecate_constant 'PLANETS', 'PLANETS_POST_2006'
+ #
+ # PLANETS.map { |planet| planet.capitalize }
+ # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
+ # (Backtrace information…)
+ # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
+ module DeprecatedConstantAccessor
+ def self.included(base)
+ extension = Module.new do
+ def const_missing(missing_const_name)
+ if class_variable_defined?(:@@_deprecated_constants)
+ if (replacement = class_variable_get(:@@_deprecated_constants)[missing_const_name.to_s])
+ replacement[:deprecator].warn(replacement[:message] || "#{name}::#{missing_const_name} is deprecated! Use #{replacement[:new]} instead.", caller_locations)
+ return ActiveSupport::Inflector.constantize(replacement[:new].to_s)
+ end
+ end
+ super
+ end
+
+ def deprecate_constant(const_name, new_constant, message: nil, deprecator: ActiveSupport::Deprecation.instance)
+ class_variable_set(:@@_deprecated_constants, {}) unless class_variable_defined?(:@@_deprecated_constants)
+ class_variable_get(:@@_deprecated_constants)[const_name.to_s] = { new: new_constant, message: message, deprecator: deprecator }
+ end
+ end
+ base.singleton_class.prepend extension
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb
index 7655fa4f99..930d71e8d2 100644
--- a/activesupport/lib/active_support/deprecation/method_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb
@@ -18,7 +18,7 @@ module ActiveSupport
#
# Using the default deprecator:
# ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
- # # => [:aaa, :bbb, :ccc]
+ # # => Fred
#
# Fred.aaa
# # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 1c6618b19a..ce39e9a232 100644
--- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
@@ -122,10 +122,11 @@ module ActiveSupport
# (Backtrace information…)
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
class DeprecatedConstantProxy < DeprecationProxy
- def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance)
+ def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
@old_const = old_const
@new_const = new_const
@deprecator = deprecator
+ @message = message
end
# Returns the class of the new constant.
@@ -143,7 +144,7 @@ module ActiveSupport
end
def warn(callstack, called, args)
- @deprecator.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack)
+ @deprecator.warn(@message, callstack)
end
end
end
diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb
index b8d200ba94..58c5c50e30 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -48,11 +48,11 @@ module ActiveSupport
private
# Outputs a deprecation warning message
#
- # ActiveSupport::Deprecation.deprecated_method_warning(:method_name)
+ # deprecated_method_warning(:method_name)
# # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon}"
- # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, :another_method)
+ # deprecated_method_warning(:method_name, :another_method)
# # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (use another_method instead)"
- # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, "Optional message")
+ # deprecated_method_warning(:method_name, "Optional message")
# # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (Optional message)"
def deprecated_method_warning(method_name, message = nil)
warning = "#{method_name} is deprecated and will be removed from #{gem_name} #{deprecation_horizon}"
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 82322291d0..99080e34a1 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -1,5 +1,8 @@
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
@@ -7,24 +10,177 @@ module ActiveSupport
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
class Duration
- EPOCH = ::Time.utc(2000)
+ class Scalar < Numeric #:nodoc:
+ attr_reader :value
+ delegate :to_i, :to_f, :to_s, to: :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def coerce(other)
+ [Scalar.new(other), self]
+ end
+
+ def -@
+ Scalar.new(-value)
+ end
+
+ def <=>(other)
+ if Scalar === other || Duration === other
+ value <=> other.value
+ elsif Numeric === other
+ value <=> other
+ else
+ nil
+ end
+ end
+
+ def +(other)
+ calculate(:+, other)
+ end
+
+ def -(other)
+ calculate(:-, other)
+ end
+
+ def *(other)
+ calculate(:*, other)
+ end
+
+ def /(other)
+ calculate(:/, other)
+ end
+
+ private
+ def calculate(op, other)
+ if Scalar === other
+ Scalar.new(value.public_send(op, other.value))
+ elsif Duration === other
+ Duration.seconds(value).public_send(op, other)
+ elsif Numeric === other
+ Scalar.new(value.public_send(op, other))
+ else
+ raise_type_error(other)
+ end
+ end
+
+ def raise_type_error(other)
+ raise TypeError, "no implicit conversion of #{other.class} into #{self.class}"
+ end
+ end
+
+ SECONDS_PER_MINUTE = 60
+ SECONDS_PER_HOUR = 3600
+ SECONDS_PER_DAY = 86400
+ SECONDS_PER_WEEK = 604800
+ SECONDS_PER_MONTH = 2629746 # 1/12 of a gregorian year
+ SECONDS_PER_YEAR = 31556952 # length of a gregorian year (365.2425 days)
+
+ PARTS_IN_SECONDS = {
+ seconds: 1,
+ minutes: SECONDS_PER_MINUTE,
+ hours: SECONDS_PER_HOUR,
+ days: SECONDS_PER_DAY,
+ weeks: SECONDS_PER_WEEK,
+ months: SECONDS_PER_MONTH,
+ years: SECONDS_PER_YEAR
+ }.freeze
attr_accessor :value, :parts
autoload :ISO8601Parser, "active_support/duration/iso8601_parser"
autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer"
+ class << self
+ # Creates a new Duration from string formatted according to ISO 8601 Duration.
+ #
+ # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
+ # This method allows negative parts to be present in pattern.
+ # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
+ def parse(iso8601duration)
+ parts = ISO8601Parser.new(iso8601duration).parse!
+ new(calculate_total_seconds(parts), parts)
+ end
+
+ def ===(other) #:nodoc:
+ other.is_a?(Duration)
+ rescue ::NoMethodError
+ false
+ end
+
+ def seconds(value) #:nodoc:
+ new(value, [[:seconds, value]])
+ end
+
+ def minutes(value) #:nodoc:
+ new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
+ end
+
+ def hours(value) #:nodoc:
+ new(value * SECONDS_PER_HOUR, [[:hours, value]])
+ end
+
+ def days(value) #:nodoc:
+ new(value * SECONDS_PER_DAY, [[:days, value]])
+ end
+
+ def weeks(value) #:nodoc:
+ new(value * SECONDS_PER_WEEK, [[:weeks, value]])
+ end
+
+ def months(value) #:nodoc:
+ new(value * SECONDS_PER_MONTH, [[:months, value]])
+ end
+
+ def years(value) #:nodoc:
+ new(value * SECONDS_PER_YEAR, [[:years, value]])
+ end
+
+ private
+
+ def calculate_total_seconds(parts)
+ parts.inject(0) do |total, (part, value)|
+ total + value * PARTS_IN_SECONDS[part]
+ end
+ end
+ end
+
def initialize(value, parts) #:nodoc:
- @value, @parts = value, parts
+ @value, @parts = value, parts.to_h
+ @parts.default = 0
+ end
+
+ def coerce(other) #:nodoc:
+ if Scalar === other
+ [other, self]
+ else
+ [Scalar.new(other), self]
+ end
+ end
+
+ # Compares one Duration with another or a Numeric to this Duration.
+ # Numeric values are treated as seconds.
+ def <=>(other)
+ if Duration === other
+ value <=> other.value
+ elsif Numeric === other
+ value <=> other
+ end
end
# Adds another Duration or a Numeric to this Duration. Numeric values
# are treated as seconds.
def +(other)
if Duration === other
- Duration.new(value + other.value, @parts + other.parts)
+ parts = @parts.dup
+ other.parts.each do |(key, value)|
+ parts[key] += value
+ end
+ Duration.new(value + other.value, parts)
else
- Duration.new(value + other, @parts + [[:seconds, other]])
+ seconds = @parts[:seconds] + other
+ Duration.new(value + other, @parts.merge(seconds: seconds))
end
end
@@ -34,6 +190,28 @@ module ActiveSupport
self + (-other)
end
+ # Multiplies this Duration by a Numeric and returns a new Duration.
+ def *(other)
+ if Scalar === other || Duration === other
+ Duration.new(value * other.value, parts.map { |type, number| [type, number * other.value] })
+ elsif Numeric === other
+ Duration.new(value * other, parts.map { |type, number| [type, number * other] })
+ else
+ raise_type_error(other)
+ end
+ end
+
+ # Divides this Duration by a Numeric and returns a new Duration.
+ def /(other)
+ if Scalar === other || Duration === other
+ Duration.new(value / other.value, parts.map { |type, number| [type, number / other.value] })
+ elsif Numeric === other
+ Duration.new(value / other, parts.map { |type, number| [type, number / other] })
+ else
+ raise_type_error(other)
+ end
+ end
+
def -@ #:nodoc:
Duration.new(-value, parts.map { |type, number| [type, -number] })
end
@@ -72,14 +250,14 @@ module ActiveSupport
# 1.day.to_i # => 86400
#
# Note that this conversion makes some assumptions about the
- # duration of some periods, e.g. months are always 30 days
- # and years are 365.25 days:
+ # duration of some periods, e.g. months are always 1/12 of year
+ # and years are 365.2425 days:
#
- # # equivalent to 30.days.to_i
- # 1.month.to_i # => 2592000
+ # # equivalent to (1.year / 12).to_i
+ # 1.month.to_i # => 2629746
#
- # # equivalent to 365.25.days.to_i
- # 1.year.to_i # => 31557600
+ # # equivalent to 365.2425.days.to_i
+ # 1.year.to_i # => 31556952
#
# In such cases, Ruby's core
# Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
@@ -99,18 +277,13 @@ module ActiveSupport
@value.hash
end
- def self.===(other) #:nodoc:
- other.is_a?(Duration)
- rescue ::NoMethodError
- false
- end
-
# Calculates a new Time or Date that is as far in the future
# as this Duration represents.
def since(time = ::Time.current)
sum(1, time)
end
alias :from_now :since
+ alias :after :since
# Calculates a new Time or Date that is as far in the past
# as this Duration represents.
@@ -118,6 +291,7 @@ module ActiveSupport
sum(-1, time)
end
alias :until :ago
+ alias :before :ago
def inspect #:nodoc:
parts.
@@ -135,27 +309,15 @@ module ActiveSupport
@value.respond_to?(method, include_private)
end
- # Creates a new Duration from string formatted according to ISO 8601 Duration.
- #
- # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
- # This method allows negative parts to be present in pattern.
- # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
- def self.parse(iso8601duration)
- parts = ISO8601Parser.new(iso8601duration).parse!
- new(EPOCH.advance(parts) - EPOCH, parts)
- end
-
# Build ISO 8601 Duration string for this duration.
# The +precision+ parameter can be used to limit seconds' precision of duration.
def iso8601(precision: nil)
ISO8601Serializer.new(self, precision: precision).serialize
end
- delegate :<=>, to: :value
-
- protected
+ private
- def sum(sign, time = ::Time.current) #:nodoc:
+ def sum(sign, time = ::Time.current)
parts.inject(time) do |t, (type, number)|
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
@@ -173,10 +335,12 @@ module ActiveSupport
end
end
- private
-
- def method_missing(method, *args, &block) #:nodoc:
+ def method_missing(method, *args, &block)
value.send(method, *args, &block)
end
+
+ def raise_type_error(other)
+ raise TypeError, "no implicit conversion of #{other.class} into #{self.class}"
+ end
end
end
diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb
index 51d53e2f8d..e5d458b3ab 100644
--- a/activesupport/lib/active_support/duration/iso8601_serializer.rb
+++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb
@@ -4,7 +4,7 @@ require "active_support/core_ext/hash/transform_values"
module ActiveSupport
class Duration
# Serializes duration to string according to ISO 8601 Duration format.
- class ISO8601Serializer
+ class ISO8601Serializer # :nodoc:
def initialize(duration, precision: nil)
@duration = duration
@precision = precision
diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb
index f54f88eb0a..f59f5d17dc 100644
--- a/activesupport/lib/active_support/evented_file_update_checker.rb
+++ b/activesupport/lib/active_support/evented_file_update_checker.rb
@@ -17,7 +17,7 @@ module ActiveSupport
#
# Example:
#
- # checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" })
+ # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
# checker.updated?
# # => false
# checker.execute_if_updated
@@ -32,6 +32,10 @@ module ActiveSupport
#
class EventedFileUpdateChecker #:nodoc: all
def initialize(files, dirs = {}, &block)
+ unless block
+ raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
+ end
+
@ph = PathHelper.new
@files = files.map { |f| @ph.xpath(f) }.to_set
@@ -121,6 +125,11 @@ module ActiveSupport
dtw.compact!
dtw.uniq!
+ normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
+ dtw = dtw.reject do |path|
+ normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) }
+ end
+
@ph.filter_out_descendants(dtw)
end
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
index 3384d12d5b..ca88e7876b 100644
--- a/activesupport/lib/active_support/execution_wrapper.rb
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -19,14 +19,14 @@ module ActiveSupport
set_callback(:complete, *args, &block)
end
- class RunHook < Struct.new(:hook) # :nodoc:
+ RunHook = Struct.new(:hook) do # :nodoc:
def before(target)
hook_state = target.send(:hook_state)
hook_state[hook] = hook.run
end
end
- class CompleteHook < Struct.new(:hook) # :nodoc:
+ CompleteHook = Struct.new(:hook) do # :nodoc:
def before(target)
hook_state = target.send(:hook_state)
if hook_state.key?(hook)
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index 2dbbfadac1..2b5e3c1350 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -38,6 +38,10 @@ module ActiveSupport
# changes. The array of files and list of directories cannot be changed
# after FileUpdateChecker has been initialized.
def initialize(files, dirs = {}, &block)
+ unless block
+ raise ArgumentError, "A block is required to initialize a FileUpdateChecker"
+ end
+
@files = files.freeze
@glob = compile_glob(dirs)
@block = block
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
index 74f2d8dd4b..371a39a5e6 100644
--- a/activesupport/lib/active_support/gem_version.rb
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -6,7 +6,7 @@ module ActiveSupport
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb
index 84eef6a623..95a86889ec 100644
--- a/activesupport/lib/active_support/gzip.rb
+++ b/activesupport/lib/active_support/gzip.rb
@@ -21,7 +21,7 @@ module ActiveSupport
# Decompresses a gzipped string.
def self.decompress(source)
- Zlib::GzipReader.new(StringIO.new(source)).read
+ Zlib::GzipReader.wrap(StringIO.new(source), &:read)
end
# Compresses a string using gzip.
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 5919b89338..1927cddf34 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -269,6 +269,10 @@ module ActiveSupport
dup.tap { |hash| hash.transform_values!(*args, &block) }
end
+ def compact
+ dup.tap(&:compact!)
+ end
+
# Convert to a regular hash with string keys.
def to_hash
_new_hash = Hash.new
@@ -280,12 +284,12 @@ module ActiveSupport
_new_hash
end
- protected
- def convert_key(key)
+ private
+ def convert_key(key) # :doc:
key.kind_of?(Symbol) ? key.to_s : key
end
- def convert_value(value, options = {})
+ def convert_value(value, options = {}) # :doc:
if value.is_a? Hash
if options[:for] == :to_hash
value.to_hash
@@ -302,7 +306,7 @@ module ActiveSupport
end
end
- def set_defaults(target)
+ def set_defaults(target) # :doc:
if default_proc
target.default_proc = default_proc.dup
else
@@ -312,4 +316,6 @@ module ActiveSupport
end
end
+# :stopdoc:
+
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 08c9ef8f02..b749913ee9 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -2,6 +2,8 @@ require "active_support"
require "active_support/file_update_checker"
require "active_support/core_ext/array/wrap"
+# :enddoc:
+
module I18n
class Railtie < Rails::Railtie
config.i18n = ActiveSupport::OrderedOptions.new
@@ -21,8 +23,6 @@ module I18n
I18n::Railtie.initialize_i18n(app)
end
- protected
-
@i18n_inited = false
# Setup i18n configuration.
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index aa68f9ec9e..c47a2e34e1 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -1,5 +1,6 @@
require "concurrent/map"
require "active_support/core_ext/array/prepend_and_append"
+require "active_support/core_ext/regexp"
require "active_support/i18n"
module ActiveSupport
@@ -43,13 +44,14 @@ module ActiveSupport
end
def add(words)
- concat(words.flatten.map(&:downcase))
- @regex_array += map { |word| to_regex(word) }
+ words = words.flatten.map(&:downcase)
+ concat(words)
+ @regex_array += words.map { |word| to_regex(word) }
self
end
def uncountable?(str)
- @regex_array.any? { |regex| regex === str }
+ @regex_array.any? { |regex| regex.match? str }
end
private
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index f515665054..737ea5666e 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -171,7 +171,7 @@ module ActiveSupport
# titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
# titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id"
def titleize(word, options = {})
- humanize(underscore(word), options).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize }
+ humanize(underscore(word), options).gsub(/\b(?<!\w['’`])[a-z]/) { |match| match.capitalize }
end
# Creates the name of a table like Rails does for models to table names.
@@ -208,10 +208,10 @@ module ActiveSupport
# Removes the module part from the expression in the string.
#
- # demodulize('ActiveRecord::CoreExtensions::String::Inflections') # => "Inflections"
- # demodulize('Inflections') # => "Inflections"
- # demodulize('::Inflections') # => "Inflections"
- # demodulize('') # => ""
+ # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
+ # demodulize('Inflections') # => "Inflections"
+ # demodulize('::Inflections') # => "Inflections"
+ # demodulize('') # => ""
#
# See also #deconstantize.
def demodulize(path)
@@ -284,7 +284,7 @@ module ActiveSupport
# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
- constant = constant.ancestors.inject do |const, ancestor|
+ constant = constant.ancestors.inject(constant) do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
const
@@ -371,7 +371,7 @@ module ActiveSupport
#
# const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
# const_regexp("::") # => "::"
- def const_regexp(camel_cased_word) #:nodoc:
+ def const_regexp(camel_cased_word)
parts = camel_cased_word.split("::".freeze)
return Regexp.escape(camel_cased_word) if parts.blank?
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index 3e78986e8e..de6dd2720b 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -57,6 +57,8 @@ module ActiveSupport
# transliterate('Jürgen')
# # => "Juergen"
def transliterate(string, replacement = "?".freeze)
+ raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
+
I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
replacement: replacement)
diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index cb6ea095b8..e2c4f33565 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -85,7 +85,7 @@ module ActiveSupport
logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
end
- protected
+ private
%w(info debug warn error fatal unknown).each do |level|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
@@ -99,7 +99,7 @@ module ActiveSupport
# option is set to +true+, it also adds bold to the string. This is based
# on the Highline implementation and will automatically append CLEAR to the
# end of the returned String.
- def color(text, color, bold = false)
+ def color(text, color, bold = false) # :doc:
return text unless colorize_logging
color = self.class.const_get(color.upcase) if color.is_a?(Symbol)
bold = bold ? BOLD : ""
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 3ba6461b57..ea09d7d2df 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -59,14 +59,14 @@ module ActiveSupport
define_method(:silence) do |level = Logger::ERROR, &block|
if logger.respond_to?(:silence)
logger.silence(level) do
- if respond_to?(:silence)
+ if defined?(super)
super(level, &block)
else
block.call(self)
end
end
else
- if respond_to?(:silence)
+ if defined?(super)
super(level, &block)
else
block.call(self)
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 7b33dc3481..24053b4fe5 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -14,10 +14,10 @@ module ActiveSupport
# where you don't want users to be able to determine the value of the payload.
#
# salt = SecureRandom.random_bytes(64)
- # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..."
- # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
- # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
- # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
+ # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, 32) # => "\x89\xE0\x156\xAC..."
+ # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
+ # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
+ # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
class MessageEncryptor
DEFAULT_CIPHER = "aes-256-cbc"
@@ -50,6 +50,11 @@ module ActiveSupport
# key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key
# derivation function.
#
+ # First additional parameter is used as the signature key for +MessageVerifier+.
+ # This allows you to specify keys to encrypt and sign data.
+ #
+ # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
+ #
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
@@ -61,7 +66,7 @@ module ActiveSupport
sign_secret = signature_key_or_options.first
@secret = secret
@sign_secret = sign_secret
- @cipher = options[:cipher] || "aes-256-cbc"
+ @cipher = options[:cipher] || DEFAULT_CIPHER
@digest = options[:digest] || "SHA1" unless aead_mode?
@verifier = resolve_verifier
@serializer = options[:serializer] || Marshal
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index 262f25b874..8c58466556 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -16,7 +16,8 @@ module ActiveSupport #:nodoc:
# through the +mb_chars+ method. Methods which would normally return a
# String object now return a Chars object so methods can be chained.
#
- # 'The Perfect String '.mb_chars.downcase.strip.normalize # => "the perfect string"
+ # 'The Perfect String '.mb_chars.downcase.strip.normalize
+ # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string">
#
# Chars objects are perfectly interchangeable with String objects as long as
# no explicit class checks are made. If certain methods do explicitly check
@@ -134,7 +135,7 @@ module ActiveSupport #:nodoc:
# Converts characters in the string to the opposite case.
#
- # 'El Cañón".mb_chars.swapcase.to_s # => "eL cAÑÓN"
+ # 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN"
def swapcase
chars Unicode.swapcase(@wrapped_string)
end
@@ -148,8 +149,8 @@ module ActiveSupport #:nodoc:
# Capitalizes the first letter of every word, when possible.
#
- # "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró"
- # "日本語".mb_chars.titleize # => "日本語"
+ # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró"
+ # "日本語".mb_chars.titleize.to_s # => "日本語"
def titleize
chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) })
end
@@ -210,9 +211,9 @@ module ActiveSupport #:nodoc:
end
end
- protected
+ private
- def translate_offset(byte_offset) #:nodoc:
+ def translate_offset(byte_offset)
return nil if byte_offset.nil?
return 0 if @wrapped_string == ""
@@ -224,7 +225,7 @@ module ActiveSupport #:nodoc:
end
end
- def chars(string) #:nodoc:
+ def chars(string)
self.class.new(string)
end
end
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 7842264b39..0912912aba 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -9,7 +9,7 @@ module ActiveSupport
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = "8.0.0"
+ UNICODE_VERSION = "9.0.0"
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -57,9 +57,12 @@ module ActiveSupport
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
- if previous == database.boundary[:cr] && current == database.boundary[:lf]
+ elsif previous == database.boundary[:cr] && current == database.boundary[:lf]
false
# GB4. (Control|CR|LF) ÷
elsif previous && in_char_class?(previous, [:control, :cr, :lf])
@@ -76,11 +79,8 @@ module ActiveSupport
# GB8. (LVT|T) X (T)
elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current
false
- # GB8a. Regional_Indicator X Regional_Indicator
- elsif database.boundary[:regional_indicator] === previous && database.boundary[:regional_indicator] === current
- false
- # GB9. X Extend
- elsif database.boundary[:extend] === current
+ # GB9. X (Extend | ZWJ)
+ elsif in_char_class?(current, [:extend, :zwj])
false
# GB9a. X SpacingMark
elsif database.boundary[:spacingmark] === current
@@ -88,7 +88,17 @@ module ActiveSupport
# GB9b. Prepend X
elsif database.boundary[:prepend] === previous
false
- # GB10. Any ÷ Any
+ # 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
@@ -358,7 +368,7 @@ module ActiveSupport
private
- def apply_mapping(string, mapping) #:nodoc:
+ def apply_mapping(string, mapping)
database.codepoints
string.each_codepoint.map do |codepoint|
cp = database.codepoints[codepoint]
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index bae5f067ae..2df819e554 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -13,7 +13,7 @@ module ActiveSupport
# To instrument an event you just need to do:
#
# ActiveSupport::Notifications.instrument('render', extra: :information) do
- # render text: 'Foo'
+ # render plain: 'Foo'
# end
#
# That first executes the block and then notifies all subscribers once done.
@@ -48,7 +48,7 @@ module ActiveSupport
# The block is saved and will be called whenever someone instruments "render":
#
# ActiveSupport::Notifications.instrument('render', extra: :information) do
- # render text: 'Foo'
+ # render plain: 'Foo'
# end
#
# event = events.first
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 6000ea44be..880340ca86 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -45,7 +45,7 @@ module ActiveSupport
#
# number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
# # => "(755) 6123-4567"
- # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/))
+ # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)
# # => "133-1234-5678"
def number_to_phone(number, options = {})
NumberToPhoneConverter.convert(number, options)
@@ -78,7 +78,7 @@ module ActiveSupport
# (defaults to "%u%n"). Fields are <tt>%u</tt> for the
# currency, and <tt>%n</tt> for the number.
# * <tt>:negative_format</tt> - Sets the format for negative
- # numbers (defaults to prepending an hyphen to the formatted
+ # numbers (defaults to prepending a hyphen to the formatted
# number given by <tt>:format</tt>). Accepts the same fields
# than <tt>:format</tt>, except <tt>%n</tt> is here the
# absolute value of the number.
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
index c485a6af63..ce363287cf 100644
--- a/activesupport/lib/active_support/number_helper/number_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -139,17 +139,17 @@ module ActiveSupport
@options ||= format_options.merge(opts)
end
- def format_options #:nodoc:
+ def format_options
default_format_options.merge!(i18n_format_options)
end
- def default_format_options #:nodoc:
+ def default_format_options
options = DEFAULTS[:format].dup
options.merge!(DEFAULTS[namespace][:format]) if namespace
options
end
- def i18n_format_options #:nodoc:
+ def i18n_format_options
locale = opts[:locale]
options = I18n.translate(:'number.format', locale: locale, default: {}).dup
@@ -160,7 +160,7 @@ module ActiveSupport
options
end
- def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
+ def translate_number_value_with_default(key, i18n_options = {})
I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options))
end
@@ -172,7 +172,7 @@ module ActiveSupport
key.split(".").reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
end
- def valid_float? #:nodoc:
+ def valid_float?
Float(number)
rescue ArgumentError, TypeError
false
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
index 9e6d8d4fd8..02431704d3 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -45,8 +45,8 @@ module ActiveSupport
Thread.current[@per_thread_registry_key] ||= new
end
- protected
- def method_missing(name, *args, &block) # :nodoc:
+ private
+ def method_missing(name, *args, &block)
# Caches the method definition as a singleton method of the receiver.
#
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index e0190fc3b0..ee6592fb5a 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -36,7 +36,7 @@ module ActiveSupport
# render xml: exception, status: 500
# end
#
- # protected
+ # private
# def deny_access
# ...
# end
diff --git a/activesupport/lib/active_support/string_inquirer.rb b/activesupport/lib/active_support/string_inquirer.rb
index 09e1cbb28d..90eac89c9e 100644
--- a/activesupport/lib/active_support/string_inquirer.rb
+++ b/activesupport/lib/active_support/string_inquirer.rb
@@ -18,7 +18,7 @@ module ActiveSupport
private
def respond_to_missing?(method_name, include_private = false)
- method_name[-1] == "?"
+ (method_name[-1] == "?") || super
end
def method_missing(method_name, *arguments)
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index c018d3e245..2924139755 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -1,4 +1,5 @@
require "active_support/per_thread_registry"
+require "active_support/notifications"
module ActiveSupport
# ActiveSupport::Subscriber is an object set to consume
@@ -51,11 +52,15 @@ 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
- def add_event_subscriber(event)
+ private
+
+ def add_event_subscriber(event) # :doc:
return if %w{ start finish }.include?(event.to_s)
pattern = "#{event}.#{namespace}"
diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb
index 3108e3e549..a18788f38e 100644
--- a/activesupport/lib/active_support/testing/autorun.rb
+++ b/activesupport/lib/active_support/testing/autorun.rb
@@ -2,8 +2,8 @@ gem "minitest"
require "minitest"
-if Minitest.respond_to?(:run_via) && !Minitest.run_via[:rails]
- Minitest.run_via[:ruby] = true
+if Minitest.respond_to?(:run_via) && !Minitest.run_via.set?
+ Minitest.run_via = :ruby
end
Minitest.autorun
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 404efe50bf..54c3263efa 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -2,7 +2,6 @@ module ActiveSupport
module Testing
module Isolation
require "thread"
- require "shellwords"
def self.included(klass) #:nodoc:
klass.class_eval do
@@ -44,7 +43,7 @@ module ActiveSupport
end
}
end
- result = Marshal.dump(self.dup)
+ result = Marshal.dump(dup)
end
write.puts [result].pack("m")
@@ -79,13 +78,15 @@ module ActiveSupport
"ISOLATION_OUTPUT" => tmpfile.path
}
- load_paths = $-I.map { |p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- orig_args = ORIG_ARGV.join(" ")
- test_opts = "-n#{self.class.name}##{Shellwords.escape(self.name)}"
- command = "#{Gem.ruby} #{load_paths} #{$0} '#{orig_args}' #{test_opts}"
+ test_opts = "-n#{self.class.name}##{name}"
- # IO.popen lets us pass env in a cross-platform way
- child = IO.popen(env, command)
+ load_path_args = []
+ $-I.each do |p|
+ load_path_args << "-I"
+ load_path_args << File.expand_path(p)
+ end
+
+ child = IO.popen([env, Gem.ruby, *load_path_args, $0, *ORIG_ARGV, test_opts])
begin
Process.wait(child.pid)
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
index e2f008b4b7..07c9be0604 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -10,7 +10,7 @@ module ActiveSupport
@stubs = Concurrent::Map.new { |h, k| h[k] = {} }
end
- def stub_object(object, method_name, return_value)
+ def stub_object(object, method_name, &block)
if stub = stubbing(object, method_name)
unstub_object(stub)
end
@@ -20,7 +20,7 @@ module ActiveSupport
@stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name)
object.singleton_class.send :alias_method, new_name, method_name
- object.define_singleton_method(method_name) { return_value }
+ object.define_singleton_method(method_name, &block)
end
def unstub_all!
@@ -134,9 +134,9 @@ module ActiveSupport
now = date_or_time.to_time.change(usec: 0)
end
- simple_stubs.stub_object(Time, :now, now)
- simple_stubs.stub_object(Date, :today, now.to_date)
- simple_stubs.stub_object(DateTime, :now, now.to_datetime)
+ simple_stubs.stub_object(Time, :now) { at(now.to_i) }
+ simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) }
+ simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) }
if block_given?
begin
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 889f71c4f3..b0dd6b7e8c 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -148,6 +148,7 @@ module ActiveSupport
"#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}"
end
alias_method :iso8601, :xmlschema
+ alias_method :rfc3339, :xmlschema
# Coerces time to a string for JSON encoding. The default format is ISO 8601.
# You can get %Y/%m/%d %H:%M:%S +offset style by setting
@@ -410,6 +411,17 @@ module ActiveSupport
@to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
end
+ # Returns an instance of +Time+, either with the same UTC offset
+ # as +self+ or in the local system timezone depending on the setting
+ # of +ActiveSupport.to_time_preserves_timezone+.
+ def to_time
+ if preserve_timezone
+ @to_time_with_instance_offset ||= getlocal(utc_offset)
+ else
+ @to_time_with_system_offset ||= getlocal
+ end
+ end
+
# So that +self+ <tt>acts_like?(:time)</tt>.
def acts_like_time?
true
@@ -427,7 +439,8 @@ module ActiveSupport
end
def freeze
- period; utc; time # preload instance variables before freezing
+ # preload instance variables before freezing
+ period; utc; time; to_datetime; to_time
super
end
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 09cb9cbbe1..ce5207546d 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -250,14 +250,21 @@ module ActiveSupport
# for time zones in the country specified by its ISO 3166-1 Alpha2 code.
def country_zones(country_code)
code = country_code.to_s.upcase
- @country_zones[code] ||=
- TZInfo::Country.get(code).zone_identifiers.map do |tz_id|
- name = MAPPING.key(tz_id)
- name && self[name]
- end.compact.sort!
+ @country_zones[code] ||= load_country_zones(code)
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)]
+ else
+ create(tz_id, nil, TZInfo::Timezone.new(tz_id))
+ end
+ end.sort!
+ end
+
def zones_map
@zones_map ||= begin
MAPPING.each_key { |place| self[place] } # load all the zones
@@ -340,6 +347,41 @@ module ActiveSupport
end
# Method for creating new ActiveSupport::TimeWithZone instance in time zone
+ # of +self+ from an ISO 8601 string.
+ #
+ # Time.zone = 'Hawaii' # => "Hawaii"
+ # Time.zone.iso8601('1999-12-31T14:00:00') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
+ #
+ # If the time components are missing then they will be set to zero.
+ #
+ # Time.zone = 'Hawaii' # => "Hawaii"
+ # Time.zone.iso8601('1999-12-31') # => Fri, 31 Dec 1999 00:00:00 HST -10:00
+ #
+ # If the string is invalid then an +ArgumentError+ will be raised unlike +parse+
+ # which returns +nil+ when given an invalid date string.
+ def iso8601(str)
+ parts = Date._iso8601(str)
+
+ raise ArgumentError, "invalid date" if parts.empty?
+
+ time = Time.new(
+ parts.fetch(:year),
+ parts.fetch(:mon),
+ parts.fetch(:mday),
+ parts.fetch(:hour, 0),
+ parts.fetch(:min, 0),
+ parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset, 0)
+ )
+
+ if parts[:offset]
+ TimeWithZone.new(time.utc, self)
+ else
+ TimeWithZone.new(nil, self, time)
+ end
+ end
+
+ # Method for creating new ActiveSupport::TimeWithZone instance in time zone
# of +self+ from parsed string.
#
# Time.zone = 'Hawaii' # => "Hawaii"
@@ -359,6 +401,36 @@ module ActiveSupport
parts_to_time(Date._parse(str, false), now)
end
+ # Method for creating new ActiveSupport::TimeWithZone instance in time zone
+ # of +self+ from an RFC 3339 string.
+ #
+ # Time.zone = 'Hawaii' # => "Hawaii"
+ # Time.zone.rfc3339('2000-01-01T00:00:00Z') # => Fri, 31 Dec 1999 14:00:00 HST -10:00
+ #
+ # If the time or zone components are missing then an +ArgumentError+ will
+ # be raised. This is much stricter than either +parse+ or +iso8601+ which
+ # allow for missing components.
+ #
+ # Time.zone = 'Hawaii' # => "Hawaii"
+ # Time.zone.rfc3339('1999-12-31') # => ArgumentError: invalid date
+ def rfc3339(str)
+ parts = Date._rfc3339(str)
+
+ raise ArgumentError, "invalid date" if parts.empty?
+
+ time = Time.new(
+ parts.fetch(:year),
+ parts.fetch(:mon),
+ parts.fetch(:mday),
+ parts.fetch(:hour),
+ parts.fetch(:min),
+ parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset)
+ )
+
+ TimeWithZone.new(time.utc, self)
+ end
+
# Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone.
#
# Assumes that +str+ is a time in the time zone +self+,
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
index dd2c178fb6..f7d9c48bbe 100644
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ b/activesupport/lib/active_support/values/unicode_tables.dat
Binary files differ
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index e16581d697..782fb41288 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -68,7 +68,17 @@ module ActiveSupport
"datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
"integer" => Proc.new { |integer| integer.to_i },
"float" => Proc.new { |float| float.to_f },
- "decimal" => Proc.new { |number| BigDecimal(number) },
+ "decimal" => Proc.new do |number|
+ if String === number
+ begin
+ BigDecimal(number)
+ rescue ArgumentError
+ BigDecimal("0")
+ end
+ else
+ BigDecimal(number)
+ end
+ 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 },
@@ -149,7 +159,7 @@ module ActiveSupport
key
end
- protected
+ private
def _dasherize(key)
# $2 must be a non-greedy regex for this to work
@@ -158,7 +168,7 @@ module ActiveSupport
end
# TODO: Add support for other encodings
- def _parse_binary(bin, entity) #:nodoc:
+ def _parse_binary(bin, entity)
case entity["encoding"]
when "base64"
::Base64.decode64(bin)
@@ -175,8 +185,6 @@ module ActiveSupport
f
end
- private
-
def current_thread_backend
Thread.current[:xml_mini_backend]
end
diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb
index 44b0bdb7dc..cde2967132 100644
--- a/activesupport/lib/active_support/xml_mini/libxml.rb
+++ b/activesupport/lib/active_support/xml_mini/libxml.rb
@@ -74,5 +74,7 @@ module LibXML #:nodoc:
end
end
+# :enddoc:
+
LibXML::XML::Document.include(LibXML::Conversions::Document)
LibXML::XML::Node.include(LibXML::Conversions::Node)
diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
index 4edb0bd2b1..8a43b05b17 100644
--- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb
+++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
@@ -73,7 +73,7 @@ module ActiveSupport
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
parser = LibXML::XML::SaxParser.io(data)
- document = self.document_class.new
+ document = document_class.new
parser.callbacks = document
parser.parse
diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
index b8b85146c5..7388bea5d8 100644
--- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
@@ -76,7 +76,7 @@ module ActiveSupport
{}
else
data.ungetc(char)
- document = self.document_class.new
+ document = document_class.new
parser = Nokogiri::XML::SAX::Parser.new(document)
parser.parse(data)
document.hash
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index 4e564591b4..c4f34c0abf 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -3,8 +3,8 @@ ORIG_ARGV = ARGV.dup
require "active_support/core_ext/kernel/reporting"
silence_warnings do
- Encoding.default_internal = "UTF-8"
- Encoding.default_external = "UTF-8"
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
end
require "active_support/testing/autorun"
@@ -24,16 +24,16 @@ ActiveSupport.to_time_preserves_timezone = ENV["PRESERVE_TIMEZONES"] == "1"
# Disable available locale checks to avoid warnings running the test suite.
I18n.enforce_available_locales = false
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
-end
-
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
-end
-
class ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ private def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ private def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb
index 4d3f5b001c..5b2bc82905 100644
--- a/activesupport/test/array_inquirer_test.rb
+++ b/activesupport/test/array_inquirer_test.rb
@@ -38,4 +38,24 @@ class ArrayInquirerTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::ArrayInquirer, result
assert_equal @array_inquirer, result
end
+
+ def test_respond_to_fallback_to_array_respond_to
+ Array.class_eval do
+ def respond_to_missing?(name, include_private = false)
+ (name == :foo) || super
+ end
+ end
+ arr = ActiveSupport::ArrayInquirer.new([:x])
+
+ assert_respond_to arr, :can_you_hear_me?
+ assert_respond_to arr, :foo
+ assert_not_respond_to arr, :nope
+ ensure
+ Array.class_eval do
+ undef_method :respond_to_missing?
+ def respond_to_missing?(name, include_private = false)
+ super
+ end
+ end
+ end
end
diff --git a/activesupport/test/autoloading_fixtures/prepend.rb b/activesupport/test/autoloading_fixtures/prepend.rb
new file mode 100644
index 0000000000..3134d1df2b
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/prepend.rb
@@ -0,0 +1,8 @@
+class SubClassConflict
+end
+
+class Prepend
+ module PrependedModule
+ end
+ prepend PrependedModule
+end
diff --git a/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb b/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb
new file mode 100644
index 0000000000..090dda3043
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/prepend/sub_class_conflict.rb
@@ -0,0 +1,2 @@
+class Prepend::SubClassConflict
+end
diff --git a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
index 829ee917d2..e477ab21d0 100644
--- a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
+++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
@@ -1,4 +1,4 @@
RaisesArbitraryException = 1
_ = A::B # Autoloading recursion, also expected to be watched and discarded.
-raise Exception, "arbitray exception message"
+raise Exception, "arbitrary exception message"
diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb
index d003a33471..184d0ebddd 100644
--- a/activesupport/test/broadcast_logger_test.rb
+++ b/activesupport/test/broadcast_logger_test.rb
@@ -69,6 +69,20 @@ module ActiveSupport
assert_equal ::Logger::FATAL, log2.local_level
end
+ test "#silence does not break custom loggers" do
+ new_logger = FakeLogger.new
+ custom_logger = CustomLogger.new
+ custom_logger.extend(Logger.broadcast(new_logger))
+
+ custom_logger.silence do
+ custom_logger.error "from error"
+ custom_logger.unknown "from unknown"
+ end
+
+ assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], custom_logger.adds
+ assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], new_logger.adds
+ end
+
test "#silence silences all loggers below the default level of ERROR" do
logger.silence do
logger.debug "test"
@@ -98,9 +112,7 @@ module ActiveSupport
assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds
end
- class FakeLogger
- include LoggerSilence
-
+ class CustomLogger
attr_reader :adds, :closed, :chevrons
attr_accessor :level, :progname, :formatter, :local_level
@@ -150,5 +162,9 @@ module ActiveSupport
@closed = true
end
end
+
+ class FakeLogger < CustomLogger
+ include LoggerSilence
+ end
end
end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index 551235e9e8..c67ffe69b8 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -47,6 +47,17 @@ module ActiveSupport
assert_raises(RuntimeError) { middleware.call({}) }
assert_nil LocalCacheRegistry.cache_for(key)
end
+
+ def test_local_cache_cleared_on_throw
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new("<3", key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), "should have a cache"
+ throw :warden
+ })
+ assert_throws(:warden) { middleware.call({}) }
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
end
end
end
@@ -316,7 +327,7 @@ module CacheStoreBehavior
def test_should_read_and_write_nil
assert @cache.write("foo", nil)
- assert_equal nil, @cache.read("foo")
+ assert_nil @cache.read("foo")
end
def test_should_read_and_write_false
@@ -464,7 +475,7 @@ module CacheStoreBehavior
Time.stub(:now, Time.at(time)) do
result = @cache.fetch("foo") do
- assert_equal nil, @cache.read("foo")
+ assert_nil @cache.read("foo")
"baz"
end
assert_equal "baz", result
@@ -476,7 +487,7 @@ module CacheStoreBehavior
@cache.write("foo", "bar", expires_in: 60)
Time.stub(:now, time + 71) do
result = @cache.fetch("foo", race_condition_ttl: 10) do
- assert_equal nil, @cache.read("foo")
+ assert_nil @cache.read("foo")
"baz"
end
assert_equal "baz", result
@@ -675,9 +686,9 @@ module LocalCacheBehavior
def test_local_cache_of_read_nil
@cache.with_local_cache do
- assert_equal nil, @cache.read("foo")
+ assert_nil @cache.read("foo")
@cache.send(:bypass_local_cache) { @cache.write "foo", "bar" }
- assert_equal nil, @cache.read("foo")
+ assert_nil @cache.read("foo")
end
end
@@ -983,7 +994,7 @@ class MemoryStoreTest < ActiveSupport::TestCase
end
def test_pruning_is_capped_at_a_max_time
- def @cache.delete_entry (*args)
+ def @cache.delete_entry(*args)
sleep(0.01)
super
end
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index aadc40ab84..4f00afb581 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -23,10 +23,6 @@ module CallbacksTest
method_name
end
- def callback_string(callback_method)
- "history << [#{callback_method.to_sym.inspect}, :string]"
- end
-
def callback_proc(callback_method)
Proc.new { |model| model.history << [callback_method, :proc] }
end
@@ -61,7 +57,6 @@ module CallbacksTest
[:before_save, :after_save].each do |callback_method|
callback_method_sym = callback_method.to_sym
send(callback_method, callback_symbol(callback_method_sym))
- ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method_sym)) }
send(callback_method, callback_proc(callback_method_sym))
send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, "")))
send(callback_method, CallbackClass)
@@ -197,10 +192,12 @@ module CallbacksTest
before_save Proc.new { |r| r.history << [:before_save, :symbol] }, unless: :no
before_save Proc.new { |r| r.history << "b00m" }, unless: :yes
# string
- 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"
+ 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
@@ -224,14 +221,54 @@ module CallbacksTest
define_callbacks :save
end
- class AroundPerson < MySuper
+ class MySlate < MySuper
attr_reader :history
attr_accessor :save_fails
+ def initialize
+ @history = []
+ end
+
+ def save
+ run_callbacks :save do
+ raise "inside save" if save_fails
+ @history << "running"
+ end
+ end
+
+ def no; false; end
+ def yes; true; end
+
+ def method_missing(sym, *)
+ case sym
+ when /^log_(.*)/
+ @history << $1
+ nil
+ when /^wrap_(.*)/
+ @history << "wrap_#$1"
+ yield
+ @history << "unwrap_#$1"
+ nil
+ when /^double_(.*)/
+ @history << "first_#$1"
+ yield
+ @history << "second_#$1"
+ yield
+ @history << "third_#$1"
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(sym)
+ sym =~ /^(log|wrap)_/ || super
+ end
+ end
+
+ class AroundPerson < MySlate
set_callback :save, :before, :nope, if: :no
set_callback :save, :before, :nope, unless: :yes
set_callback :save, :after, :tweedle
- ActiveSupport::Deprecation.silence { set_callback :save, :before, "tweedle_dee" }
set_callback :save, :before, proc { |m| m.history << "yup" }
set_callback :save, :before, :nope, if: proc { false }
set_callback :save, :before, :nope, unless: proc { true }
@@ -242,9 +279,6 @@ module CallbacksTest
set_callback :save, :around, :w0tno, if: :no
set_callback :save, :around, :tweedle_deedle
- def no; false; end
- def yes; true; end
-
def nope
@history << "boom"
end
@@ -264,10 +298,6 @@ module CallbacksTest
yield
end
- def tweedle_dee
- @history << "tweedle dee"
- end
-
def tweedle_dum
@history << "tweedle dum pre"
yield
@@ -283,17 +313,6 @@ module CallbacksTest
yield
@history << "tweedle deedle post"
end
-
- def initialize
- @history = []
- end
-
- def save
- run_callbacks :save do
- raise "inside save" if save_fails
- @history << "running"
- end
- end
end
class AroundPersonResult < MySuper
@@ -394,7 +413,6 @@ module CallbacksTest
around = AroundPerson.new
around.save
assert_equal [
- "tweedle dee",
"yup", "yup",
"tweedle dum pre",
"w0tyes before",
@@ -408,6 +426,32 @@ module CallbacksTest
end
end
+ class DoubleYieldTest < ActiveSupport::TestCase
+ class DoubleYieldModel < MySlate
+ set_callback :save, :around, :wrap_outer
+ set_callback :save, :around, :double_trouble
+ set_callback :save, :around, :wrap_inner
+ end
+
+ def test_double_save
+ double = DoubleYieldModel.new
+ double.save
+ assert_equal [
+ "wrap_outer",
+ "first_trouble",
+ "wrap_inner",
+ "running",
+ "unwrap_inner",
+ "second_trouble",
+ "wrap_inner",
+ "running",
+ "unwrap_inner",
+ "third_trouble",
+ "unwrap_outer",
+ ], double.history
+ end
+ end
+
class CallStackTest < ActiveSupport::TestCase
def test_tidy_call_stack
around = AroundPerson.new
@@ -487,7 +531,6 @@ module CallbacksTest
assert_equal [], person.history
person.save
assert_equal [
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :block],
@@ -495,7 +538,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], person.history
end
@@ -514,7 +556,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], person.history
end
@@ -527,7 +568,6 @@ module CallbacksTest
person.save
assert_equal [
[:before_save, :symbol],
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :class],
@@ -536,7 +576,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], person.history
end
@@ -830,37 +869,11 @@ module CallbacksTest
end
end
- class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase
- def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set
- obj = CallbackFalseTerminator.new
- obj.save
- assert_equal nil, obj.halted
- assert obj.saved
- end
- end
-
- class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase
- def setup
- ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true
- end
-
- def test_returning_false_does_not_halt_callback_if_config_variable_is_true
- obj = CallbackFalseTerminator.new
- obj.save
- assert_equal nil, obj.halted
- assert obj.saved
- end
- end
-
- class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase
- def setup
- ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false
- end
-
- def test_returning_false_does_not_halt_callback_if_config_variable_is_false
+ class CallbackFalseTerminatorTest < ActiveSupport::TestCase
+ def test_returning_false_does_not_halt_callback
obj = CallbackFalseTerminator.new
obj.save
- assert_equal nil, obj.halted
+ assert_nil obj.halted
assert obj.saved
end
end
@@ -886,7 +899,6 @@ module CallbacksTest
writer.save
assert_equal [
[:before_save, :symbol],
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :class],
@@ -895,7 +907,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], writer.history
end
@@ -987,7 +998,7 @@ module CallbacksTest
set_callback :foo, :before, :foo, if: callback
def run; run_callbacks :foo; end
private
- def foo; end
+ def foo; end
}
object = klass.new
object.run
@@ -1109,14 +1120,6 @@ module CallbacksTest
assert_equal 1, calls.length
end
- def test_add_eval
- calls = []
- klass = ActiveSupport::Deprecation.silence { build_class("bar") }
- klass.class_eval { define_method(:bar) { calls << klass } }
- klass.new.run
- assert_equal 1, calls.length
- end
-
def test_skip_class # removes one at a time
calls = []
callback = Class.new {
@@ -1151,7 +1154,7 @@ module CallbacksTest
def test_skip_string # raises error
calls = []
- klass = ActiveSupport::Deprecation.silence { build_class("bar") }
+ klass = build_class(:bar)
klass.class_eval { define_method(:bar) { calls << klass } }
assert_raises(ArgumentError) { klass.skip "bar" }
klass.new.run
@@ -1178,11 +1181,22 @@ module CallbacksTest
end
class DeprecatedWarningTest < ActiveSupport::TestCase
- def test_deprecate_string_callback
+ def test_deprecate_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" }
+ end
+ end
+
+ class NotPermittedStringCallbackTest < ActiveSupport::TestCase
+ def test_passing_string_callback_is_not_permitted
klass = Class.new(Record)
- assert_deprecated do
- klass.send :before_save, "tweedle_dee"
+ assert_raises(ArgumentError) do
+ klass.before_save "tweedle"
end
end
end
diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb
index c618fea81a..004b4dc9ce 100644
--- a/activesupport/test/class_cache_test.rb
+++ b/activesupport/test/class_cache_test.rb
@@ -61,7 +61,7 @@ module ActiveSupport
def test_safe_get_constantizes_doesnt_fail_on_invalid_names
assert @cache.empty?
- assert_equal nil, @cache.safe_get("OmgTotallyInvalidConstantName")
+ assert_nil @cache.safe_get("OmgTotallyInvalidConstantName")
end
def test_new_rejects_strings
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index 95507c815d..7a5a5414a7 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -76,7 +76,7 @@ class ConcernTest < ActiveSupport::TestCase
end
def test_class_methods_are_extended_only_on_expected_objects
- ::Object.__send__(:include, Qux)
+ ::Object.include(Qux)
Object.extend(Qux::ClassMethods)
# module needs to be created after Qux is included in Object or bug won't
# be triggered
diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index af2db8c991..32b720bcbb 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -73,6 +73,11 @@ module ConstantizeTestCases
yield("RaisesNoMethodError")
end
end
+
+ with_autoloading_fixtures do
+ yield("Prepend::SubClassConflict")
+ assert_equal "constant", defined?(Prepend::SubClassConflict)
+ end
end
def run_safe_constantize_tests_on
diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb
index a9c44907cc..a7905196ae 100644
--- a/activesupport/test/core_ext/class_test.rb
+++ b/activesupport/test/core_ext/class_test.rb
@@ -25,4 +25,14 @@ class ClassTest < ActiveSupport::TestCase
assert_equal [Baz], Bar.subclasses
assert_equal [], Baz.subclasses
end
+
+ def test_descendants_excludes_singleton_classes
+ klass = Parent.new.singleton_class
+ refute 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"
+ end
end
diff --git a/activesupport/test/core_ext/date_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb
index 4c90460032..6c6205a4d2 100644
--- a/activesupport/test/core_ext/date_and_time_compatibility_test.rb
+++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb
@@ -16,11 +16,13 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_time_to_time_preserves_timezone
with_preserve_timezone(true) do
with_env_tz "US/Eastern" do
- time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time
+ source = Time.new(2016, 4, 23, 15, 11, 12, 3600)
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
assert_equal @utc_offset, time.utc_offset
+ assert_equal source.object_id, time.object_id
end
end
end
@@ -28,11 +30,43 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_time_to_time_does_not_preserve_time_zone
with_preserve_timezone(false) do
with_env_tz "US/Eastern" do
- time = Time.new(2016, 4, 23, 15, 11, 12, 3600).to_time
+ source = Time.new(2016, 4, 23, 15, 11, 12, 3600)
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
assert_equal @system_offset, time.utc_offset
+ assert_not_equal source.object_id, time.object_id
+ end
+ end
+ end
+
+ def test_time_to_time_frozen_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ source = Time.new(2016, 4, 23, 15, 11, 12, 3600).freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ assert_equal source.object_id, time.object_id
+ assert_predicate time, :frozen?
+ end
+ end
+ end
+
+ def test_time_to_time_frozen_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ source = Time.new(2016, 4, 23, 15, 11, 12, 3600).freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ assert_not_equal source.object_id, time.object_id
+ assert_not_predicate time, :frozen?
end
end
end
@@ -40,7 +74,8 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_datetime_to_time_preserves_timezone
with_preserve_timezone(true) do
with_env_tz "US/Eastern" do
- time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)).to_time
+ source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24))
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
@@ -52,7 +87,8 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_datetime_to_time_does_not_preserve_time_zone
with_preserve_timezone(false) do
with_env_tz "US/Eastern" do
- time = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)).to_time
+ source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24))
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
@@ -61,17 +97,47 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
end
end
+ def test_datetime_to_time_frozen_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)).freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
+ end
+ end
+ end
+
+ def test_datetime_to_time_frozen_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ source = DateTime.new(2016, 4, 23, 15, 11, 12, Rational(1, 24)).freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
+ end
+ end
+ end
+
def test_twz_to_time_preserves_timezone
with_preserve_timezone(true) do
with_env_tz "US/Eastern" do
- time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time
+ source = ActiveSupport::TimeWithZone.new(@utc_time, @zone)
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
assert_instance_of Time, time.getutc
assert_equal @utc_offset, time.utc_offset
- time = ActiveSupport::TimeWithZone.new(@date_time, @zone).to_time
+ source = ActiveSupport::TimeWithZone.new(@date_time, @zone)
+ time = source.to_time
assert_instance_of Time, time
assert_equal @date_time, time.getutc
@@ -84,19 +150,69 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_twz_to_time_does_not_preserve_time_zone
with_preserve_timezone(false) do
with_env_tz "US/Eastern" do
- time = ActiveSupport::TimeWithZone.new(@utc_time, @zone).to_time
+ source = ActiveSupport::TimeWithZone.new(@utc_time, @zone)
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_instance_of Time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+
+ source = ActiveSupport::TimeWithZone.new(@date_time, @zone)
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @date_time, time.getutc
+ assert_instance_of Time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_twz_to_time_frozen_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ source = ActiveSupport::TimeWithZone.new(@utc_time, @zone).freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_instance_of Time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
+
+ source = ActiveSupport::TimeWithZone.new(@date_time, @zone).freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @date_time, time.getutc
+ assert_instance_of Time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
+ end
+ end
+ end
+
+ def test_twz_to_time_frozen_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ source = ActiveSupport::TimeWithZone.new(@utc_time, @zone).freeze
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
assert_instance_of Time, time.getutc
assert_equal @system_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
- time = ActiveSupport::TimeWithZone.new(@date_time, @zone).to_time
+ source = ActiveSupport::TimeWithZone.new(@date_time, @zone).freeze
+ time = source.to_time
assert_instance_of Time, time
assert_equal @date_time, time.getutc
assert_instance_of Time, time.getutc
assert_equal @system_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
end
end
end
@@ -104,7 +220,8 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_string_to_time_preserves_timezone
with_preserve_timezone(true) do
with_env_tz "US/Eastern" do
- time = "2016-04-23T15:11:12+01:00".to_time
+ source = "2016-04-23T15:11:12+01:00"
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
@@ -116,11 +233,40 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase
def test_string_to_time_does_not_preserve_time_zone
with_preserve_timezone(false) do
with_env_tz "US/Eastern" do
- time = "2016-04-23T15:11:12+01:00".to_time
+ source = "2016-04-23T15:11:12+01:00"
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @system_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_string_to_time_frozen_preserves_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ source = "2016-04-23T15:11:12+01:00".freeze
+ time = source.to_time
+
+ assert_instance_of Time, time
+ assert_equal @utc_time, time.getutc
+ assert_equal @utc_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
+ end
+ end
+ end
+
+ def test_string_to_time_frozen_does_not_preserve_time_zone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ source = "2016-04-23T15:11:12+01:00".freeze
+ time = source.to_time
assert_instance_of Time, time
assert_equal @utc_time, time.getutc
assert_equal @system_offset, time.utc_offset
+ assert_not_predicate time, :frozen?
end
end
end
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index 5a90210bb8..50bb1004f7 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -395,6 +395,10 @@ class DateExtBehaviorTest < ActiveSupport::TestCase
assert Date.new.acts_like_date?
end
+ def test_blank?
+ assert_not Date.new.blank?
+ end
+
def test_freeze_doesnt_clobber_memoized_instance_methods
assert_nothing_raised do
Date.today.freeze.inspect
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 81777889ec..36f0ee22b8 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -4,8 +4,8 @@ require "core_ext/date_and_time_behavior"
require "time_zone_test_helpers"
class DateTimeExtCalculationsTest < ActiveSupport::TestCase
- def date_time_init(year, month, day, hour, minute, second, *args)
- DateTime.civil(year, month, day, hour, minute, second)
+ def date_time_init(year, month, day, hour, minute, second, usec = 0)
+ DateTime.civil(year, month, day, hour, minute, second + (usec / 1000000))
end
include DateAndTimeBehavior
@@ -113,7 +113,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_end_of_day
- assert_equal DateTime.civil(2005, 2, 4, 23, 59, 59), DateTime.civil(2005, 2, 4, 10, 10, 10).end_of_day
+ assert_equal DateTime.civil(2005, 2, 4, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 10, 10, 10).end_of_day
end
def test_beginning_of_hour
@@ -121,7 +121,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_end_of_hour
- assert_equal DateTime.civil(2005, 2, 4, 19, 59, 59), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_hour
+ assert_equal DateTime.civil(2005, 2, 4, 19, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_hour
end
def test_beginning_of_minute
@@ -129,13 +129,13 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_end_of_minute
- assert_equal DateTime.civil(2005, 2, 4, 19, 30, 59), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_minute
+ assert_equal DateTime.civil(2005, 2, 4, 19, 30, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 4, 19, 30, 10).end_of_minute
end
def test_end_of_month
- assert_equal DateTime.civil(2005, 3, 31, 23, 59, 59), DateTime.civil(2005, 3, 20, 10, 10, 10).end_of_month
- assert_equal DateTime.civil(2005, 2, 28, 23, 59, 59), DateTime.civil(2005, 2, 20, 10, 10, 10).end_of_month
- assert_equal DateTime.civil(2005, 4, 30, 23, 59, 59), DateTime.civil(2005, 4, 20, 10, 10, 10).end_of_month
+ assert_equal DateTime.civil(2005, 3, 31, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 3, 20, 10, 10, 10).end_of_month
+ assert_equal DateTime.civil(2005, 2, 28, 23, 59, Rational(59999999999, 1000000000)), DateTime.civil(2005, 2, 20, 10, 10, 10).end_of_month
+ 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
@@ -162,12 +162,19 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2006, 2, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(year: 2006)
assert_equal DateTime.civil(2005, 6, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(month: 6)
assert_equal DateTime.civil(2012, 9, 22, 15, 15, 10), DateTime.civil(2005, 2, 22, 15, 15, 10).change(year: 2012, month: 9)
- assert_equal DateTime.civil(2005, 2, 22, 16), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16)
- assert_equal DateTime.civil(2005, 2, 22, 16, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45)
- assert_equal DateTime.civil(2005, 2, 22, 15, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(min: 45)
+ assert_equal DateTime.civil(2005, 2, 22, 16), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16)
+ assert_equal DateTime.civil(2005, 2, 22, 16, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45)
+ assert_equal DateTime.civil(2005, 2, 22, 15, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(min: 45)
# datetime with fractions of a second
assert_equal DateTime.civil(2005, 2, 1, 15, 15, 10.7), DateTime.civil(2005, 2, 22, 15, 15, 10.7).change(day: 1)
+ assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(usec: 8)
+ assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(nsec: 8000)
+ assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 1, nsec: 1) }
+ assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 1000000) }
+ assert_raise(ArgumentError) { DateTime.civil(2005, 1, 2, 11, 22, 0).change(nsec: 1000000000) }
+ assert_nothing_raised { DateTime.civil(2005, 1, 2, 11, 22, 0).change(usec: 999999) }
+ assert_nothing_raised { DateTime.civil(2005, 1, 2, 11, 22, 0).change(nsec: 999999999) }
end
def test_advance
@@ -318,6 +325,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert DateTime.new.acts_like_time?
end
+ def test_blank?
+ assert_not DateTime.new.blank?
+ end
+
def test_utc?
assert_equal true, DateTime.civil(2005, 2, 21, 10, 11, 12).utc?
assert_equal true, DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc?
@@ -376,7 +387,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
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_equal nil, DateTime.civil(2000) <=> "Invalid as Time"
+ assert_nil DateTime.civil(2000) <=> "Invalid as Time"
end
def test_compare_with_integer
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index b7b4a9dd00..1648a9b270 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -21,7 +21,7 @@ class DurationTest < ActiveSupport::TestCase
end
def test_instance_of
- assert 1.minute.instance_of?(Fixnum)
+ assert 1.minute.instance_of?(1.class)
assert 2.days.instance_of?(ActiveSupport::Duration)
assert !3.second.instance_of?(Numeric)
end
@@ -84,6 +84,38 @@ class DurationTest < ActiveSupport::TestCase
assert_nothing_raised { Date.today - Date.today }
end
+ def test_plus
+ assert_equal 2.seconds, 1.second + 1.second
+ assert_instance_of ActiveSupport::Duration, 1.second + 1.second
+ assert_equal 2.seconds, 1.second + 1
+ assert_instance_of ActiveSupport::Duration, 1.second + 1
+ end
+
+ def test_minus
+ assert_equal 1.second, 2.seconds - 1.second
+ assert_instance_of ActiveSupport::Duration, 2.seconds - 1.second
+ assert_equal 1.second, 2.seconds - 1
+ assert_instance_of ActiveSupport::Duration, 2.seconds - 1
+ assert_equal 1.second, 2 - 1.second
+ assert_instance_of ActiveSupport::Duration, 2.seconds - 1
+ end
+
+ def test_multiply
+ assert_equal 7.days, 1.day * 7
+ assert_instance_of ActiveSupport::Duration, 1.day * 7
+ assert_equal 86400, 1.day * 1.second
+ end
+
+ def test_divide
+ assert_equal 1.day, 7.days / 7
+ assert_instance_of ActiveSupport::Duration, 7.days / 7
+ assert_equal 1, 1.day / 1.day
+ end
+
+ def test_date_added_with_multiplied_duration
+ assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 1.day * 2
+ end
+
def test_plus_with_time
assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration"
end
@@ -179,6 +211,19 @@ class DurationTest < ActiveSupport::TestCase
Time.zone = nil
end
+ def test_before_and_afer
+ t = Time.local(2000)
+ assert_equal t + 1, 1.second.after(t)
+ assert_equal t - 1, 1.second.before(t)
+ end
+
+ def test_before_and_after_without_argument
+ Time.stub(:now, Time.local(2000)) do
+ assert_equal Time.now - 1.second, 1.second.before
+ assert_equal Time.now + 1.second, 1.second.after
+ end
+ end
+
def test_adding_hours_across_dst_boundary
with_env_tz "CET" do
assert_equal Time.local(2009, 3, 29, 0, 0, 0) + 24.hours, Time.local(2009, 3, 30, 1, 0, 0)
@@ -196,7 +241,7 @@ class DurationTest < ActiveSupport::TestCase
assert_nothing_raised do
1.minute.times { counter += 1 }
end
- assert_equal counter, 60
+ assert_equal 60, counter
end
def test_as_json
@@ -213,7 +258,7 @@ class DurationTest < ActiveSupport::TestCase
when 1.day
"ok"
end
- assert_equal cased, "ok"
+ assert_equal "ok", cased
end
def test_respond_to
@@ -237,6 +282,141 @@ class DurationTest < ActiveSupport::TestCase
assert_equal(1, (61 <=> 1.minute))
end
+ def test_implicit_coercion
+ assert_equal 2.days, 2 * 1.day
+ assert_instance_of ActiveSupport::Duration, 2 * 1.day
+ assert_equal Time.utc(2017, 1, 3), Time.utc(2017, 1, 1) + 2 * 1.day
+ assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 2 * 1.day
+ end
+
+ def test_scalar_coerce
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+ assert_instance_of ActiveSupport::Duration::Scalar, 10 + scalar
+ assert_instance_of ActiveSupport::Duration, 10.seconds + scalar
+ end
+
+ def test_scalar_delegations
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+ assert_kind_of Float, scalar.to_f
+ assert_kind_of Integer, scalar.to_i
+ assert_kind_of String, scalar.to_s
+ end
+
+ def test_scalar_unary_minus
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal(-10, -scalar)
+ assert_instance_of ActiveSupport::Duration::Scalar, -scalar
+ end
+
+ def test_scalar_compare
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal(1, scalar <=> 5)
+ assert_equal(0, scalar <=> 10)
+ assert_equal(-1, scalar <=> 15)
+ assert_equal(nil, scalar <=> "foo")
+ end
+
+ def test_scalar_plus
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal 20, 10 + scalar
+ assert_instance_of ActiveSupport::Duration::Scalar, 10 + scalar
+ assert_equal 20, scalar + 10
+ assert_instance_of ActiveSupport::Duration::Scalar, scalar + 10
+ assert_equal 20, 10.seconds + scalar
+ assert_instance_of ActiveSupport::Duration, 10.seconds + scalar
+ assert_equal 20, scalar + 10.seconds
+ assert_instance_of ActiveSupport::Duration, scalar + 10.seconds
+
+ exception = assert_raises(TypeError) do
+ scalar + "foo"
+ end
+
+ assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
+ end
+
+ def test_scalar_minus
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal 10, 20 - scalar
+ assert_instance_of ActiveSupport::Duration::Scalar, 20 - scalar
+ assert_equal 5, scalar - 5
+ assert_instance_of ActiveSupport::Duration::Scalar, scalar - 5
+ assert_equal 10, 20.seconds - scalar
+ assert_instance_of ActiveSupport::Duration, 20.seconds - scalar
+ assert_equal 5, scalar - 5.seconds
+ assert_instance_of ActiveSupport::Duration, scalar - 5.seconds
+
+ exception = assert_raises(TypeError) do
+ scalar - "foo"
+ end
+
+ assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
+ end
+
+ def test_scalar_multiply
+ scalar = ActiveSupport::Duration::Scalar.new(5)
+
+ assert_equal 10, 2 * scalar
+ assert_instance_of ActiveSupport::Duration::Scalar, 2 * scalar
+ assert_equal 10, scalar * 2
+ assert_instance_of ActiveSupport::Duration::Scalar, scalar * 2
+ assert_equal 10, 2.seconds * scalar
+ assert_instance_of ActiveSupport::Duration, 2.seconds * scalar
+ assert_equal 10, scalar * 2.seconds
+ assert_instance_of ActiveSupport::Duration, scalar * 2.seconds
+
+ exception = assert_raises(TypeError) do
+ scalar * "foo"
+ end
+
+ assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
+ end
+
+ def test_scalar_divide
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal 10, 100 / scalar
+ assert_instance_of ActiveSupport::Duration::Scalar, 100 / scalar
+ assert_equal 5, scalar / 2
+ assert_instance_of ActiveSupport::Duration::Scalar, scalar / 2
+ assert_equal 10, 100.seconds / scalar
+ assert_instance_of ActiveSupport::Duration, 2.seconds * scalar
+ assert_equal 5, scalar / 2.seconds
+ assert_instance_of ActiveSupport::Duration, scalar / 2.seconds
+
+ exception = assert_raises(TypeError) do
+ scalar / "foo"
+ end
+
+ assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
+ end
+
+ def test_twelve_months_equals_one_year
+ assert_equal 12.months, 1.year
+ end
+
+ def test_thirty_days_does_not_equal_one_month
+ assert_not_equal 30.days, 1.month
+ end
+
+ def test_adding_one_month_maintains_day_of_month
+ (1..11).each do |month|
+ [1, 14, 28].each do |day|
+ assert_equal Date.civil(2016, month + 1, day), Date.civil(2016, month, day) + 1.month
+ end
+ end
+
+ assert_equal Date.civil(2017, 1, 1), Date.civil(2016, 12, 1) + 1.month
+ assert_equal Date.civil(2017, 1, 14), Date.civil(2016, 12, 14) + 1.month
+ assert_equal Date.civil(2017, 1, 28), Date.civil(2016, 12, 28) + 1.month
+
+ assert_equal Date.civil(2015, 2, 28), Date.civil(2015, 1, 31) + 1.month
+ assert_equal Date.civil(2016, 2, 29), Date.civil(2016, 1, 31) + 1.month
+ end
+
# ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb
# published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE
#
@@ -345,6 +525,32 @@ class DurationTest < ActiveSupport::TestCase
end
end
+ def test_iso8601_parsing_equivalence_with_numeric_extensions_over_long_periods
+ with_env_tz eastern_time_zone do
+ with_tz_default "Eastern Time (US & Canada)" do
+ assert_equal 3.months, ActiveSupport::Duration.parse("P3M")
+ assert_equal 3.months.to_i, ActiveSupport::Duration.parse("P3M").to_i
+ assert_equal 10.months, ActiveSupport::Duration.parse("P10M")
+ assert_equal 10.months.to_i, ActiveSupport::Duration.parse("P10M").to_i
+ assert_equal 3.years, ActiveSupport::Duration.parse("P3Y")
+ assert_equal 3.years.to_i, ActiveSupport::Duration.parse("P3Y").to_i
+ assert_equal 10.years, ActiveSupport::Duration.parse("P10Y")
+ assert_equal 10.years.to_i, ActiveSupport::Duration.parse("P10Y").to_i
+ end
+ end
+ end
+
+ def test_adding_durations_do_not_hold_prior_states
+ time = Time.parse("Nov 29, 2016")
+ # If the implementation adds and subtracts 3 months, the
+ # resulting date would have been in February so the day will
+ # change to the 29th.
+ d1 = 3.months - 3.months
+ d2 = 2.months - 2.months
+
+ assert_equal time + d1, time + d2
+ end
+
private
def eastern_time_zone
if Gem.win_platform?
diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb
index 5b839f1032..4f1ab993b8 100644
--- a/activesupport/test/core_ext/enumerable_test.rb
+++ b/activesupport/test/core_ext/enumerable_test.rb
@@ -172,7 +172,7 @@ class EnumerableTests < ActiveSupport::TestCase
payments.index_by(&:price))
assert_equal Enumerator, payments.index_by.class
if Enumerator.method_defined? :size
- assert_equal nil, payments.index_by.size
+ assert_nil payments.index_by.size
assert_equal 42, (1..42).index_by.size
end
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index 8b816b3aad..525ea08abd 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -8,6 +8,8 @@ require "active_support/core_ext/object/deep_dup"
require "active_support/inflections"
class HashExtTest < ActiveSupport::TestCase
+ HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
+
class IndifferentHash < ActiveSupport::HashWithIndifferentAccess
end
@@ -111,7 +113,7 @@ class HashExtTest < ActiveSupport::TestCase
transformed_hash = @mixed.dup
transformed_hash.transform_keys! { |key| key.to_s.upcase }
assert_equal @upcase_strings, transformed_hash
- assert_equal @mixed, :a => 1, "b" => 2
+ assert_equal({ :a => 1, "b" => 2 }, @mixed)
end
def test_deep_transform_keys!
@@ -127,7 +129,7 @@ class HashExtTest < ActiveSupport::TestCase
transformed_hash = @nested_mixed.deep_dup
transformed_hash.deep_transform_keys! { |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, transformed_hash
- assert_equal @nested_mixed, "a" => { b: { "c" => 3 } }
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
end
def test_symbolize_keys
@@ -167,7 +169,7 @@ class HashExtTest < ActiveSupport::TestCase
transformed_hash = @mixed.dup
transformed_hash.deep_symbolize_keys!
assert_equal @symbols, transformed_hash
- assert_equal @mixed, :a => 1, "b" => 2
+ assert_equal({ :a => 1, "b" => 2 }, @mixed)
end
def test_deep_symbolize_keys!
@@ -183,7 +185,7 @@ class HashExtTest < ActiveSupport::TestCase
transformed_hash = @nested_mixed.deep_dup
transformed_hash.deep_symbolize_keys!
assert_equal @nested_symbols, transformed_hash
- assert_equal @nested_mixed, "a" => { b: { "c" => 3 } }
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
end
def test_symbolize_keys_preserves_keys_that_cant_be_symbolized
@@ -243,7 +245,7 @@ class HashExtTest < ActiveSupport::TestCase
transformed_hash = @mixed.dup
transformed_hash.stringify_keys!
assert_equal @strings, transformed_hash
- assert_equal @mixed, :a => 1, "b" => 2
+ assert_equal({ :a => 1, "b" => 2 }, @mixed)
end
def test_deep_stringify_keys!
@@ -259,7 +261,7 @@ class HashExtTest < ActiveSupport::TestCase
transformed_hash = @nested_mixed.deep_dup
transformed_hash.deep_stringify_keys!
assert_equal @nested_strings, transformed_hash
- assert_equal @nested_mixed, "a" => { b: { "c" => 3 } }
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
end
def test_symbolize_keys_for_hash_with_indifferent_access
@@ -390,8 +392,8 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 1, hash[:a]
assert_equal true, hash[:b]
assert_equal false, hash[:c]
- assert_equal nil, hash[:d]
- assert_equal nil, hash[:e]
+ assert_nil hash[:d]
+ assert_nil hash[:e]
end
def test_indifferent_reading_with_nonnil_default
@@ -404,7 +406,7 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 1, hash[:a]
assert_equal true, hash[:b]
assert_equal false, hash[:c]
- assert_equal nil, hash[:d]
+ assert_nil hash[:d]
assert_equal 1, hash[:e]
end
@@ -414,11 +416,11 @@ class HashExtTest < ActiveSupport::TestCase
hash["b"] = 2
hash[3] = 3
- assert_equal hash["a"], 1
- assert_equal hash["b"], 2
- assert_equal hash[:a], 1
- assert_equal hash[:b], 2
- assert_equal hash[3], 3
+ assert_equal 1, hash["a"]
+ assert_equal 2, hash["b"]
+ assert_equal 1, hash[:a]
+ assert_equal 2, hash[:b]
+ assert_equal 3, hash[3]
end
def test_indifferent_update
@@ -430,16 +432,16 @@ class HashExtTest < ActiveSupport::TestCase
updated_with_symbols = hash.update(@symbols)
updated_with_mixed = hash.update(@mixed)
- assert_equal updated_with_strings[:a], 1
- assert_equal updated_with_strings["a"], 1
- assert_equal updated_with_strings["b"], 2
+ assert_equal 1, updated_with_strings[:a]
+ assert_equal 1, updated_with_strings["a"]
+ assert_equal 2, updated_with_strings["b"]
- assert_equal updated_with_symbols[:a], 1
- assert_equal updated_with_symbols["b"], 2
- assert_equal updated_with_symbols[:b], 2
+ assert_equal 1, updated_with_symbols[:a]
+ assert_equal 2, updated_with_symbols["b"]
+ assert_equal 2, updated_with_symbols[:b]
- assert_equal updated_with_mixed[:a], 1
- assert_equal updated_with_mixed["b"], 2
+ assert_equal 1, updated_with_mixed[:a]
+ assert_equal 2, updated_with_mixed["b"]
assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 }
end
@@ -447,7 +449,7 @@ class HashExtTest < ActiveSupport::TestCase
def test_update_with_to_hash_conversion
hash = HashWithIndifferentAccess.new
hash.update HashByConversion.new(a: 1)
- assert_equal hash["a"], 1
+ assert_equal 1, hash["a"]
end
def test_indifferent_merging
@@ -472,7 +474,7 @@ class HashExtTest < ActiveSupport::TestCase
def test_merge_with_to_hash_conversion
hash = HashWithIndifferentAccess.new
merged = hash.merge HashByConversion.new(a: 1)
- assert_equal merged["a"], 1
+ assert_equal 1, merged["a"]
end
def test_indifferent_replace
@@ -536,11 +538,11 @@ class HashExtTest < ActiveSupport::TestCase
def test_indifferent_deleting
get_hash = proc { { a: "foo" }.with_indifferent_access }
hash = get_hash.call
- assert_equal hash.delete(:a), "foo"
- assert_equal hash.delete(:a), nil
+ assert_equal "foo", hash.delete(:a)
+ assert_nil hash.delete(:a)
hash = get_hash.call
- assert_equal hash.delete("a"), "foo"
- assert_equal hash.delete("a"), nil
+ assert_equal "foo", hash.delete("a")
+ assert_nil hash.delete("a")
end
def test_indifferent_select
@@ -589,6 +591,26 @@ class HashExtTest < ActiveSupport::TestCase
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)
+ compacted_hash = hash.compact
+
+ assert_equal(@strings, compacted_hash)
+ assert_equal(hash_contain_nil_value, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, compacted_hash
+
+ empty_hash = ActiveSupport::HashWithIndifferentAccess.new
+ compacted_hash = empty_hash.compact
+
+ assert_equal compacted_hash, empty_hash
+
+ non_empty_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: :bar)
+ compacted_hash = non_empty_hash.compact
+
+ assert_equal compacted_hash, non_empty_hash
+ end
+
def test_indifferent_to_hash
# Should convert to a Hash with String keys.
assert_equal @strings, @mixed.with_indifferent_access.to_hash
@@ -933,8 +955,8 @@ class HashExtTest < ActiveSupport::TestCase
extracted = original.extract!(:a, :x)
assert_equal expected, extracted
- assert_equal nil, extracted[:a]
- assert_equal nil, extracted[:x]
+ assert_nil extracted[:a]
+ assert_nil extracted[:x]
end
def test_indifferent_extract
@@ -1017,7 +1039,7 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal({}, h)
h = @symbols.dup
- assert_equal(nil, h.compact!)
+ assert_nil(h.compact!)
assert_equal(@symbols, h)
end
@@ -1068,6 +1090,30 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 1, hash[:a]
assert_equal 3, hash[:b]
end
+
+ def test_inheriting_from_top_level_hash_with_indifferent_access_preserves_ancestors_chain
+ klass = Class.new(::HashWithIndifferentAccess)
+ assert_equal ActiveSupport::HashWithIndifferentAccess, klass.ancestors[1]
+ end
+
+ def test_inheriting_from_hash_with_indifferent_access_properly_dumps_ivars
+ klass = Class.new(::HashWithIndifferentAccess) do
+ def initialize(*)
+ @foo = "bar"
+ super
+ end
+ end
+
+ yaml_output = klass.new.to_yaml
+
+ # `hash-with-ivars` was introduced in 2.0.9 (https://git.io/vyUQW)
+ if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("2.0.9")
+ assert_includes yaml_output, "hash-with-ivars"
+ assert_includes yaml_output, "@foo: bar"
+ else
+ assert_includes yaml_output, "hash"
+ end
+ end
end
class IWriteMyOwnXML
@@ -1113,6 +1159,8 @@ class HashExtToParamTests < ActiveSupport::TestCase
end
class HashToXmlTest < ActiveSupport::TestCase
+ HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
+
def setup
@xml_options = { root: :person, skip_instruct: true, indent: 0 }
end
@@ -1595,7 +1643,7 @@ class HashToXmlTest < ActiveSupport::TestCase
def test_should_return_nil_if_no_key_is_supplied
hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
- assert_equal nil, hash_wia.default
+ assert_nil hash_wia.default
end
def test_should_use_default_value_for_unknown_key
diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb
index db0008b735..26f5088ede 100644
--- a/activesupport/test/core_ext/kernel_test.rb
+++ b/activesupport/test/core_ext/kernel_test.rb
@@ -49,19 +49,3 @@ class KernelSuppressTest < ActiveSupport::TestCase
suppress(LoadError, ArgumentError) { raise ArgumentError }
end
end
-
-class MockStdErr
- attr_reader :output
- def puts(message)
- @output ||= []
- @output << message
- end
-
- def info(message)
- puts(message)
- end
-
- def write(message)
- puts(message)
- end
-end
diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb
index a899f98705..cabeed2fae 100644
--- a/activesupport/test/core_ext/marshal_test.rb
+++ b/activesupport/test/core_ext/marshal_test.rb
@@ -19,6 +19,19 @@ class MarshalTest < ActiveSupport::TestCase
end
end
+ test "that Marshal#load still works when passed a proc" do
+ example_string = "test"
+
+ example_proc = Proc.new do |o|
+ if o.is_a?(String)
+ o.capitalize!
+ end
+ end
+
+ dumped = Marshal.dump(example_string)
+ assert_equal Marshal.load(dumped, example_proc), "Test"
+ end
+
test "that a missing class is autoloaded from string" do
dumped = nil
with_autoloading_fixtures do
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 b816fa50e3..af240bc38d 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
@@ -121,11 +121,11 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
def test_should_not_affect_superclass_if_subclass_set_value
@class.foo = "super"
- assert_equal @class.foo, "super"
+ assert_equal "super", @class.foo
assert_nil @subclass.foo
@subclass.foo = "sub"
- assert_equal @class.foo, "super"
- assert_equal @subclass.foo, "sub"
+ assert_equal "super", @class.foo
+ assert_equal "sub", @subclass.foo
end
end
diff --git a/activesupport/test/core_ext/module/attribute_aliasing_test.rb b/activesupport/test/core_ext/module/attribute_aliasing_test.rb
index d8c2dfd6b8..fdfa868851 100644
--- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb
+++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb
@@ -52,8 +52,8 @@ class AttributeAliasingTest < ActiveSupport::TestCase
assert_equal "No, really, this is not a joke.", e.Data
assert e.Data?
- e.Data = "Uppercased methods are teh suck"
- assert_equal "Uppercased methods are teh suck", e.body
+ e.Data = "Uppercased methods are the suck"
+ assert_equal "Uppercased methods are the suck", e.body
assert e.body?
end
end
diff --git a/activesupport/test/core_ext/module/introspection_test.rb b/activesupport/test/core_ext/module/introspection_test.rb
new file mode 100644
index 0000000000..db383850cd
--- /dev/null
+++ b/activesupport/test/core_ext/module/introspection_test.rb
@@ -0,0 +1,37 @@
+require "abstract_unit"
+require "active_support/core_ext/module/introspection"
+
+module ParentA
+ module B
+ module C; end
+ module FrozenC; end
+ FrozenC.freeze
+ end
+
+ module FrozenB; end
+ FrozenB.freeze
+end
+
+class IntrospectionTest < ActiveSupport::TestCase
+ def test_parent_name
+ assert_equal "ParentA", ParentA::B.parent_name
+ assert_equal "ParentA::B", ParentA::B::C.parent_name
+ assert_nil ParentA.parent_name
+ end
+
+ def test_parent_name_when_frozen
+ assert_equal "ParentA", ParentA::FrozenB.parent_name
+ assert_equal "ParentA::B", ParentA::B::FrozenC.parent_name
+ end
+
+ def test_parent
+ assert_equal ParentA::B, ParentA::B::C.parent
+ assert_equal ParentA, ParentA::B.parent
+ assert_equal Object, ParentA.parent
+ end
+
+ def test_parents
+ assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.parents
+ assert_equal [ParentA, Object], ParentA::B.parents
+ end
+end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index 8f2df22f27..a17438bf4d 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -1,30 +1,11 @@
require "abstract_unit"
require "active_support/core_ext/module"
-module One
- Constant1 = "Hello World"
- Constant2 = "What's up?"
-end
-
-class Ab
- include One
- Constant1 = "Hello World" # Will have different object id than One::Constant1
- Constant3 = "Goodbye World"
-end
-
-module Yz
- module Zy
- class Cd
- include One
- end
- end
-end
-
Somewhere = Struct.new(:street, :city) do
attr_accessor :name
end
-class Someone < Struct.new(:name, :place)
+Someone = Struct.new(:name, :place) do
delegate :street, :city, :to_f, to: :place
delegate :name=, to: :place, prefix: true
delegate :upcase, to: "place.city"
@@ -35,10 +16,10 @@ class Someone < Struct.new(:name, :place)
"some_table"
end
- FAILED_DELEGATE_LINE = __LINE__ + 1
+ self::FAILED_DELEGATE_LINE = __LINE__ + 1
delegate :foo, to: :place
- FAILED_DELEGATE_LINE_2 = __LINE__ + 1
+ self::FAILED_DELEGATE_LINE_2 = __LINE__ + 1
delegate :bar, to: :place, allow_nil: true
private
@@ -210,21 +191,21 @@ class ModuleTest < ActiveSupport::TestCase
def test_delegation_prefix
invoice = Invoice.new(@david)
- assert_equal invoice.client_name, "David"
- assert_equal invoice.client_street, "Paulina"
- assert_equal invoice.client_city, "Chicago"
+ assert_equal "David", invoice.client_name
+ assert_equal "Paulina", invoice.client_street
+ assert_equal "Chicago", invoice.client_city
end
def test_delegation_custom_prefix
invoice = Invoice.new(@david)
- assert_equal invoice.customer_name, "David"
- assert_equal invoice.customer_street, "Paulina"
- assert_equal invoice.customer_city, "Chicago"
+ assert_equal "David", invoice.customer_name
+ assert_equal "Paulina", invoice.customer_street
+ assert_equal "Chicago", invoice.customer_city
end
def test_delegation_prefix_with_nil_or_false
- assert_equal Developer.new(@david).name, "David"
- assert_equal Tester.new(@david).name, "David"
+ assert_equal "David", Developer.new(@david).name
+ assert_equal "David", Tester.new(@david).name
end
def test_delegation_prefix_with_instance_variable
@@ -240,7 +221,7 @@ class ModuleTest < ActiveSupport::TestCase
def test_delegation_with_allow_nil
rails = Project.new("Rails", Someone.new("David"))
- assert_equal rails.name, "David"
+ assert_equal "David", rails.name
end
def test_delegation_with_allow_nil_and_nil_value
@@ -375,17 +356,6 @@ class ModuleTest < ActiveSupport::TestCase
assert_match(/undefined method `my_fake_method' for/, e.message)
end
- def test_parent
- assert_equal Yz::Zy, Yz::Zy::Cd.parent
- assert_equal Yz, Yz::Zy.parent
- assert_equal Object, Yz.parent
- end
-
- def test_parents
- assert_equal [Yz::Zy, Yz, Object], Yz::Zy::Cd.parents
- assert_equal [Yz, Object], Yz::Zy.parents
- end
-
def test_delegate_with_case
event = Event.new(Tester.new)
assert_equal 1, event.foo
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 5361b7b708..3cfbe6e7e6 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -12,7 +12,7 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
10.minutes => 600,
1.hour + 15.minutes => 4500,
2.days + 4.hours + 30.minutes => 189000,
- 5.years + 1.month + 1.fortnight => 161589600
+ 5.years + 1.month + 1.fortnight => 161624106
}
end
@@ -61,10 +61,10 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_duration_after_conversion_is_no_longer_accurate
- assert_equal 30.days.to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now)
- assert_equal 365.25.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now)
- assert_equal 30.days.to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow)
- assert_equal 365.25.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow)
+ assert_equal (1.year / 12).to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now)
+ assert_equal 365.2425.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now)
+ assert_equal (1.year / 12).to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow)
+ assert_equal 365.2425.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow)
end
def test_add_one_year_to_leap_day
@@ -394,6 +394,10 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal "1000010.0", BigDecimal("1000010").to_s
assert_equal "10000 10.0", BigDecimal("1000010").to_s("5F")
+
+ assert_raises TypeError do
+ 1.to_s({})
+ end
end
def test_in_milliseconds
diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb
index ab0676524e..7fd3fed042 100644
--- a/activesupport/test/core_ext/object/blank_test.rb
+++ b/activesupport/test/core_ext/object/blank_test.rb
@@ -15,7 +15,7 @@ class BlankTest < ActiveSupport::TestCase
end
BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ]
- NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 } ]
+ NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now ]
def test_blank
BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" }
@@ -28,7 +28,7 @@ class BlankTest < ActiveSupport::TestCase
end
def test_presence
- BLANK.each { |v| assert_equal nil, v.presence, "#{v.inspect}.presence should return nil" }
+ BLANK.each { |v| assert_nil v.presence, "#{v.inspect}.presence should return nil" }
NOT.each { |v| assert_equal v, v.presence, "#{v.inspect}.presence should return self" }
end
end
diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb
index e335ec1b40..f247ee16de 100644
--- a/activesupport/test/core_ext/object/deep_dup_test.rb
+++ b/activesupport/test/core_ext/object/deep_dup_test.rb
@@ -6,7 +6,7 @@ class DeepDupTest < ActiveSupport::TestCase
array = [1, [2, 3]]
dup = array.deep_dup
dup[1][2] = 4
- assert_equal nil, array[1][2]
+ assert_nil array[1][2]
assert_equal 4, dup[1][2]
end
@@ -14,7 +14,7 @@ class DeepDupTest < ActiveSupport::TestCase
hash = { a: { b: "b" } }
dup = hash.deep_dup
dup[:a][:c] = "c"
- assert_equal nil, hash[:a][:c]
+ assert_nil hash[:a][:c]
assert_equal "c", dup[:a][:c]
end
@@ -22,7 +22,7 @@ class DeepDupTest < ActiveSupport::TestCase
array = [1, { a: 2, b: 3 } ]
dup = array.deep_dup
dup[1][:c] = 4
- assert_equal nil, array[1][:c]
+ assert_nil array[1][:c]
assert_equal 4, dup[1][:c]
end
@@ -30,7 +30,7 @@ class DeepDupTest < ActiveSupport::TestCase
hash = { a: [1, 2] }
dup = hash.deep_dup
dup[:a][2] = "c"
- assert_equal nil, hash[:a][2]
+ assert_nil hash[:a][2]
assert_equal "c", dup[:a][2]
end
diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
index 677e32db1d..68b0129980 100644
--- a/activesupport/test/core_ext/object/duplicable_test.rb
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -4,16 +4,26 @@ require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/numeric/time"
class DuplicableTest < ActiveSupport::TestCase
- RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts)]
- ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new]
- ALLOW_DUP << BigDecimal.new("4.56")
+ 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]
+ 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")]
+ end
def test_duplicable
rubinius_skip "* Method#dup is allowed at the moment on Rubinius\n" \
"* https://github.com/rubinius/rubinius/issues/3089"
RAISE_DUP.each do |v|
- assert !v.duplicable?
+ assert !v.duplicable?, "#{ v.inspect } should not be duplicable"
assert_raises(TypeError, v.class.name) { v.dup }
end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index b0ebe0af8e..a98951e889 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -1,5 +1,6 @@
require "date"
require "abstract_unit"
+require "timeout"
require "inflector_test_cases"
require "constantize_test_cases"
@@ -158,37 +159,37 @@ class StringInflectionsTest < ActiveSupport::TestCase
def test_string_parameterized_normal
StringToParameterized.each do |normal, slugged|
- assert_equal(normal.parameterize, slugged)
+ assert_equal(slugged, normal.parameterize)
end
end
def test_string_parameterized_normal_preserve_case
StringToParameterizedPreserveCase.each do |normal, slugged|
- assert_equal(normal.parameterize(preserve_case: true), slugged)
+ assert_equal(slugged, normal.parameterize(preserve_case: true))
end
end
def test_string_parameterized_no_separator
StringToParameterizeWithNoSeparator.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: ""), slugged)
+ assert_equal(slugged, normal.parameterize(separator: ""))
end
end
def test_string_parameterized_no_separator_preserve_case
StringToParameterizePreserveCaseWithNoSeparator.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: "", preserve_case: true), slugged)
+ assert_equal(slugged, normal.parameterize(separator: "", preserve_case: true))
end
end
def test_string_parameterized_underscore
StringToParameterizeWithUnderscore.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: "_"), slugged)
+ assert_equal(slugged, normal.parameterize(separator: "_"))
end
end
def test_string_parameterized_underscore_preserve_case
StringToParameterizePreserceCaseWithUnderscore.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: "_", preserve_case: true), slugged)
+ assert_equal(slugged, normal.parameterize(separator: "_", preserve_case: true))
end
end
@@ -234,7 +235,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
original = %{\u205f\u3000 A string surrounded by various unicode spaces,
with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007}
- expected = "A string surrounded by various unicode spaces, " +
+ expected = "A string surrounded by various unicode spaces, " \
"with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )."
# Make sure squish returns what we expect:
@@ -295,7 +296,7 @@ class StringInflectionsTest < ActiveSupport::TestCase
def test_truncate_words_with_complex_string
Timeout.timeout(10) do
complex_string = "aa aa aaa aa aaa aaa aaa aa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaaa aaaaa aaaaa aaaaaa aa aa aa aaa aa aaa aa aa aa aa a aaa aaa \n a aaa <<s"
- assert_equal complex_string.truncate_words(80), complex_string
+ assert_equal complex_string, complex_string.truncate_words(80)
end
rescue Timeout::Error
assert false
@@ -351,7 +352,7 @@ class StringAccessTest < ActiveSupport::TestCase
test "#at with Regex, returns the matching portion of the string" do
assert_equal "lo", "hello".at(/lo/)
- assert_equal nil, "hello".at(/nonexisting/)
+ assert_nil "hello".at(/nonexisting/)
end
test "#from with positive Integer, returns substring from the given position to the end" do
@@ -722,14 +723,14 @@ class OutputSafetyTest < ActiveSupport::TestCase
test "Prepending safe onto unsafe yields unsafe" do
@string.prepend "other".html_safe
assert !@string.html_safe?
- assert_equal @string, "otherhello"
+ 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_equal other, "&lt;foo&gt;other"
+ assert_equal "&lt;foo&gt;other", other
end
test "Concatting safe onto unsafe yields unsafe" do
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index c11804c3dc..bd644c8457 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -569,6 +569,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
Time::DATE_FORMATS.delete(:custom)
end
+ def test_rfc3339_with_fractional_seconds
+ time = Time.new(1999, 12, 31, 19, 0, Rational(1, 8), -18000)
+ assert_equal "1999-12-31T19:00:00.125-05:00", time.rfc3339(3)
+ end
+
def test_to_date
assert_equal Date.new(2005, 2, 21), Time.local(2005, 2, 21, 17, 44, 30).to_date
end
@@ -771,7 +776,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
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_equal nil, Time.utc(2000) <=> "Invalid as Time"
+ assert_nil Time.utc(2000) <=> "Invalid as Time"
end
def test_at_with_datetime
@@ -910,6 +915,37 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
def test_all_year
assert_equal Time.local(2011, 1, 1, 0, 0, 0)..Time.local(2011, 12, 31, 23, 59, 59, Rational(999999999, 1000)), Time.local(2011, 6, 7, 10, 10, 10).all_year
end
+
+ def test_rfc3339_parse
+ time = Time.rfc3339("1999-12-31T19:00:00.125-05:00")
+
+ assert_equal 1999, time.year
+ assert_equal 12, time.month
+ assert_equal 31, time.day
+ assert_equal 19, time.hour
+ assert_equal 0, time.min
+ assert_equal 0, time.sec
+ assert_equal 125000, time.usec
+ assert_equal(-18000, time.utc_offset)
+
+ exception = assert_raises(ArgumentError) do
+ Time.rfc3339("1999-12-31")
+ end
+
+ assert_equal "invalid date", exception.message
+
+ exception = assert_raises(ArgumentError) do
+ Time.rfc3339("1999-12-31T19:00:00")
+ end
+
+ assert_equal "invalid date", exception.message
+
+ exception = assert_raises(ArgumentError) do
+ Time.rfc3339("foobar")
+ end
+
+ assert_equal "invalid date", exception.message
+ end
end
class TimeExtMarshalingTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 620084f27d..c3afe68378 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -144,6 +144,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil)
end
+ def test_iso8601_with_fractional_seconds
+ @twz += Rational(1, 8)
+ assert_equal "1999-12-31T19:00:00.125-05:00", @twz.iso8601(3)
+ end
+
+ def test_rfc3339_with_fractional_seconds
+ @twz += Rational(1, 8)
+ assert_equal "1999-12-31T19:00:00.125-05:00", @twz.rfc3339(3)
+ end
+
def test_to_yaml
yaml = <<-EOF.strip_heredoc
--- !ruby/object:ActiveSupport::TimeWithZone
@@ -421,11 +431,29 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal time, Time.at(time)
end
- def test_to_time
- with_env_tz "US/Eastern" do
- assert_equal Time, @twz.to_time.class
- assert_equal Time.local(1999, 12, 31, 19), @twz.to_time
- assert_equal Time.local(1999, 12, 31, 19).utc_offset, @twz.to_time.utc_offset
+ def test_to_time_with_preserve_timezone
+ with_preserve_timezone(true) do
+ with_env_tz "US/Eastern" do
+ time = @twz.to_time
+
+ assert_equal Time, time.class
+ assert_equal time.object_id, @twz.to_time.object_id
+ assert_equal Time.local(1999, 12, 31, 19), time
+ assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset
+ end
+ end
+ end
+
+ def test_to_time_without_preserve_timezone
+ with_preserve_timezone(false) do
+ with_env_tz "US/Eastern" do
+ time = @twz.to_time
+
+ assert_equal Time, time.class
+ assert_equal time.object_id, @twz.to_time.object_id
+ assert_equal Time.local(1999, 12, 31, 19), time
+ assert_equal Time.local(1999, 12, 31, 19).utc_offset, time.utc_offset
+ end
end
end
@@ -455,6 +483,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal false, ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:date)
end
+ def test_blank?
+ assert_not @twz.blank?
+ end
+
def test_is_a
assert_kind_of Time, @twz
assert_kind_of Time, @twz
@@ -503,6 +535,8 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_nothing_raised do
@twz.period
@twz.time
+ @twz.to_datetime
+ @twz.to_time
end
end
@@ -1057,7 +1091,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
Time.zone = -9.hours
assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone
Time.zone = nil
- assert_equal nil, Time.zone
+ assert_nil Time.zone
end
def test_time_zone_getter_and_setter_with_zone_default_set
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index f333a46d75..e38d4e83e5 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -121,7 +121,7 @@ class DependenciesTest < ActiveSupport::TestCase
silence_warnings { require_dependency filename }
assert_equal 2, $check_warnings_load_count
- assert_equal nil, $checked_verbose, "After first load warnings should be left alone."
+ assert_nil $checked_verbose, "After first load warnings should be left alone."
assert_includes ActiveSupport::Dependencies.loaded, expanded
ActiveSupport::Dependencies.clear
@@ -271,7 +271,8 @@ class DependenciesTest < ActiveSupport::TestCase
def test_raising_discards_autoloaded_constants
with_autoloading_fixtures do
- assert_raises(Exception, "arbitray exception message") { RaisesArbitraryException }
+ e = assert_raises(Exception) { RaisesArbitraryException }
+ assert_equal("arbitrary exception message", e.message)
assert_not defined?(A)
assert_not defined?(RaisesArbitraryException)
end
@@ -397,14 +398,17 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
- def failing_test_access_thru_and_upwards_fails
- with_autoloading_fixtures do
- assert_not defined?(ModuleFolder)
- assert_raise(NameError) { ModuleFolder::Object }
- assert_raise(NameError) { ModuleFolder::NestedClass::Object }
+ # This raises only on 2.5.. (warns on ..2.4)
+ if RUBY_VERSION > "2.5"
+ def test_access_thru_and_upwards_fails
+ with_autoloading_fixtures do
+ assert_not defined?(ModuleFolder)
+ assert_raise(NameError) { ModuleFolder::Object }
+ assert_raise(NameError) { ModuleFolder::NestedClass::Object }
+ end
+ ensure
+ remove_constants(:ModuleFolder)
end
- ensure
- remove_constants(:ModuleFolder)
end
def test_non_existing_const_raises_name_error_with_fully_qualified_name
@@ -526,8 +530,8 @@ class DependenciesTest < ActiveSupport::TestCase
def test_file_search
with_loading "dependencies" do
root = ActiveSupport::Dependencies.autoload_paths.first
- assert_equal nil, ActiveSupport::Dependencies.search_for_file("service_three")
- assert_equal nil, ActiveSupport::Dependencies.search_for_file("service_three.rb")
+ assert_nil ActiveSupport::Dependencies.search_for_file("service_three")
+ assert_nil ActiveSupport::Dependencies.search_for_file("service_three.rb")
assert_equal root + "/service_one.rb", ActiveSupport::Dependencies.search_for_file("service_one")
assert_equal root + "/service_one.rb", ActiveSupport::Dependencies.search_for_file("service_one.rb")
end
@@ -548,7 +552,6 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal autoload + "/conflict.rb", ActiveSupport::Dependencies.search_for_file("conflict")
end
-
end
def test_custom_const_missing_should_work
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index 5be93f3a1a..36d1ef0849 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -35,6 +35,18 @@ class Deprecatee
A = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Deprecatee::A", "Deprecatee::B::C")
end
+class DeprecateeWithAccessor
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
+
+ module B
+ C = 1
+ end
+ deprecate_constant "A", "DeprecateeWithAccessor::B::C"
+
+ class NewException < StandardError; end
+ deprecate_constant "OldException", "DeprecateeWithAccessor::NewException"
+end
+
class DeprecationTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
@@ -162,6 +174,17 @@ class DeprecationTest < ActiveSupport::TestCase
assert_not_deprecated { assert_equal Deprecatee::B::C.class, Deprecatee::A.class }
end
+ def test_deprecated_constant_accessor
+ assert_not_deprecated { DeprecateeWithAccessor::B::C }
+ assert_deprecated("DeprecateeWithAccessor::A") { assert_equal DeprecateeWithAccessor::B::C, DeprecateeWithAccessor::A }
+ end
+
+ def test_deprecated_constant_accessor_exception
+ raise DeprecateeWithAccessor::NewException.new("Test")
+ rescue DeprecateeWithAccessor::OldException => e
+ assert_kind_of DeprecateeWithAccessor::NewException, e
+ end
+
def test_assert_deprecated_raises_when_method_not_deprecated
assert_raises(Minitest::Assertion) { assert_deprecated { @dtc.not } }
end
@@ -285,6 +308,16 @@ class DeprecationTest < ActiveSupport::TestCase
end
end
+ def test_deprecated_constant_with_custom_message
+ deprecator = deprecator_with_messages
+
+ klass = Class.new
+ klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new("klass::OLD", "Object", deprecator, message: "foo"))
+
+ klass::OLD.to_s
+ assert_match "foo", deprecator.messages.last
+ end
+
def test_deprecated_instance_variable_with_instance_deprecator
deprecator = deprecator_with_messages
diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb
index 09c5ce1f07..cf349d53ee 100644
--- a/activesupport/test/descendants_tracker_test_cases.rb
+++ b/activesupport/test/descendants_tracker_test_cases.rb
@@ -40,7 +40,7 @@ module DescendantsTrackerTestCases
end
end
- protected
+ private
def assert_equal_sets(expected, actual)
assert_equal Set.new(expected), Set.new(actual)
diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb
index 77d8dcb0f8..f33a5f5764 100644
--- a/activesupport/test/evented_file_update_checker_test.rb
+++ b/activesupport/test/evented_file_update_checker_test.rb
@@ -7,6 +7,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
def setup
skip if ENV["LISTEN"] == "0"
+ require "listen"
super
end
@@ -30,11 +31,6 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
wait # wait for the events to fire
end
- def rm_f(files)
- super
- wait
- end
-
test "notifies forked processes" do
jruby_skip "Forking not available on JRuby"
@@ -43,7 +39,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
checker = new_checker(tmpfiles) {}
assert !checker.updated?
- # Pipes used for flow controll across fork.
+ # Pipes used for flow control across fork.
boot_reader, boot_writer = IO.pipe
touch_reader, touch_writer = IO.pipe
diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb
index 03ec6020c3..7fefc066b3 100644
--- a/activesupport/test/executor_test.rb
+++ b/activesupport/test/executor_test.rb
@@ -105,7 +105,7 @@ class ExecutorTest < ActiveSupport::TestCase
executor.wrap {}
- assert_equal nil, supplied_state
+ assert_nil supplied_state
end
def test_exception_skips_uninvoked_hook
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
index 48cd387196..361e7e2349 100644
--- a/activesupport/test/file_update_checker_shared_tests.rb
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -273,4 +273,10 @@ module FileUpdateCheckerSharedTests
assert checker.execute_if_updated
assert_equal 2, i
end
+
+ test "initialize raises an ArgumentError if no block given" do
+ assert_raise ArgumentError do
+ new_checker([])
+ end
+ end
end
diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb
index f51d3cdf65..33e0cd2a04 100644
--- a/activesupport/test/gzip_test.rb
+++ b/activesupport/test/gzip_test.rb
@@ -30,4 +30,14 @@ class GzipTest < ActiveSupport::TestCase
assert_equal true, (gzipped_by_best_compression.bytesize < gzipped_by_speed.bytesize)
end
+
+ def test_decompress_checks_crc
+ compressed = ActiveSupport::Gzip.compress("Hello World")
+ first_crc_byte_index = compressed.bytesize - 8
+ compressed.setbyte(first_crc_byte_index, compressed.getbyte(first_crc_byte_index) ^ 0xff)
+
+ assert_raises(Zlib::GzipFile::CRCError) do
+ ActiveSupport::Gzip.decompress(compressed)
+ end
+ end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 4a3f64ef57..14bc10513b 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -31,6 +31,32 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal "", ActiveSupport::Inflector.pluralize("")
end
+ test "uncountability of ascii word" do
+ word = "HTTP"
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable word
+ end
+
+ assert_equal word, ActiveSupport::Inflector.pluralize(word)
+ assert_equal word, ActiveSupport::Inflector.singularize(word)
+ assert_equal ActiveSupport::Inflector.pluralize(word), ActiveSupport::Inflector.singularize(word)
+
+ ActiveSupport::Inflector.inflections.uncountables.pop
+ end
+
+ test "uncountability of non-ascii word" do
+ word = "猫"
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable word
+ end
+
+ assert_equal word, ActiveSupport::Inflector.pluralize(word)
+ assert_equal word, ActiveSupport::Inflector.singularize(word)
+ assert_equal ActiveSupport::Inflector.pluralize(word), ActiveSupport::Inflector.singularize(word)
+
+ ActiveSupport::Inflector.inflections.uncountables.pop
+ end
+
ActiveSupport::Inflector.inflections.uncountable.each do |word|
define_method "test_uncountability_of_#{word}" do
assert_equal word, ActiveSupport::Inflector.singularize(word)
@@ -252,35 +278,25 @@ class InflectorTest < ActiveSupport::TestCase
end
end
- # FIXME: get following tests to pass on jruby, currently skipped
- #
- # Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes
- # required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby
- # causing our tests to error out.
- # related bug http://jira.codehaus.org/browse/JRUBY-7194
def test_parameterize
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_and_normalize
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizedAndNormalized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_with_custom_separator
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizeWithUnderscore.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: "_"))
end
end
def test_parameterize_with_multi_character_separator
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string.gsub("-", "__sep__"), ActiveSupport::Inflector.parameterize(some_string, separator: "__sep__"))
end
@@ -520,12 +536,4 @@ class InflectorTest < ActiveSupport::TestCase
end
end
end
-
- def test_inflections_with_uncountable_words
- ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable "HTTP"
- end
-
- assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP")
- end
end
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index 20a1c5b35d..d61ca3fc18 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -286,6 +286,7 @@ module InflectorTestCases
"¿por qué?" => "¿Por Qué?",
"Fred’s" => "Fred’s",
"Fred`s" => "Fred`s",
+ "this was 'fake news'" => "This Was 'Fake News'",
ActiveSupport::SafeBuffer.new("confirmation num") => "Confirmation Num"
}
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index 6d1d8f1b95..6f5051c312 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -75,12 +75,17 @@ class TestJSONDecoding < ActiveSupport::TestCase
}
TESTS.each_with_index do |(json, expected), index|
+ fail_message = "JSON decoding failed for #{json}"
+
test "json decodes #{index}" do
with_tz_default "Eastern Time (US & Canada)" do
with_parse_json_times(true) do
silence_warnings do
- assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
- failed for #{json}"
+ if expected.nil?
+ assert_nil ActiveSupport::JSON.decode(json), fail_message
+ else
+ assert_equal expected, ActiveSupport::JSON.decode(json), fail_message
+ end
end
end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index fa518104c2..6d8f7cfbd0 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -329,7 +329,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_nil_true_and_false_represented_as_themselves
- assert_equal nil, nil.as_json
+ assert_nil nil.as_json
assert_equal true, true.as_json
assert_equal false, false.as_json
end
@@ -452,7 +452,7 @@ EXPECTED
assert_equal '{"number":null}', NaNNumber.new.to_json
end
- protected
+ private
def object_keys(json_object)
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
diff --git a/activesupport/test/json/encoding_test_cases.rb b/activesupport/test/json/encoding_test_cases.rb
index 8eac246937..7e4775cec8 100644
--- a/activesupport/test/json/encoding_test_cases.rb
+++ b/activesupport/test/json/encoding_test_cases.rb
@@ -1,4 +1,8 @@
require "bigdecimal"
+require "date"
+require "time"
+require "pathname"
+require "uri"
module JSONTest
class Foo
@@ -23,7 +27,7 @@ module JSONTest
end
end
- class MyStruct < Struct.new(:name, :value)
+ MyStruct = Struct.new(:name, :value) do
def initialize(*)
@unused = "unused instance variable"
super
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index d56a46b250..d6109c761d 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -80,6 +80,6 @@ class MessageVerifierTest < ActiveSupport::TestCase
exception = assert_raise(ArgumentError) do
ActiveSupport::MessageVerifier.new(nil)
end
- assert_equal exception.message, "Secret should not be nil."
+ assert_equal "Secret should not be nil.", exception.message
end
end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 2ee4a1ce68..d80d340986 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -50,7 +50,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase
assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length
end
- def test_forwarded_method_with_non_string_result_should_be_returned_vertabim
+ def test_forwarded_method_with_non_string_result_should_be_returned_verbatim
str = ""
str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end }
@chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end }
@@ -231,7 +231,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert_equal 0, @chars.index("こに")
assert_equal 2, @chars.index("ち")
assert_equal 2, @chars.index("ち", -2)
- assert_equal nil, @chars.index("ち", -1)
+ assert_nil @chars.index("ち", -1)
assert_equal 3, @chars.index("わ")
assert_equal 5, "ééxééx".mb_chars.index("x", 4)
end
@@ -390,11 +390,11 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_slice_should_take_character_offsets
- assert_equal nil, "".mb_chars.slice(0)
+ assert_nil "".mb_chars.slice(0)
assert_equal "こ", @chars.slice(0)
assert_equal "わ", @chars.slice(3)
- assert_equal nil, "".mb_chars.slice(-1..1)
- assert_equal nil, "".mb_chars.slice(-1, 1)
+ assert_nil "".mb_chars.slice(-1..1)
+ assert_nil "".mb_chars.slice(-1, 1)
assert_equal "", "".mb_chars.slice(0..10)
assert_equal "にちわ", @chars.slice(1..3)
assert_equal "にちわ", @chars.slice(1, 3)
@@ -403,10 +403,10 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert_equal "", @chars.slice(4..10)
assert_equal "に", @chars.slice(/に/u)
assert_equal "にち", @chars.slice(/に./u)
- assert_equal nil, @chars.slice(/unknown/u)
+ assert_nil @chars.slice(/unknown/u)
assert_equal "にち", @chars.slice(/(にち)/u, 1)
- assert_equal nil, @chars.slice(/(にち)/u, 2)
- assert_equal nil, @chars.slice(7..6)
+ assert_nil @chars.slice(/(にち)/u, 2)
+ assert_nil @chars.slice(7..6)
end
def test_slice_bang_returns_sliced_out_substring
@@ -414,7 +414,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_slice_bang_returns_nil_on_out_of_bound_arguments
- assert_equal nil, @chars.mb_chars.slice!(9..10)
+ assert_nil @chars.mb_chars.slice!(9..10)
end
def test_slice_bang_removes_the_slice_from_the_receiver
@@ -664,7 +664,6 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
end
def test_tidy_bytes_should_tidy_bytes
-
single_byte_cases = {
"\x21" => "!", # Valid ASCII byte, low
"\x41" => "A", # Valid ASCII byte, mid
diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb
index bf004d7924..ef1a26135f 100644
--- a/activesupport/test/multibyte_conformance_test.rb
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -82,7 +82,7 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
end
end
- protected
+ private
def each_line_of_norm_tests(&block)
File.open(File.join(CACHE_DIR, UNIDATA_FILE), "r") do | f |
until f.eof?
diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb
index 04a7d290d9..b3328987ae 100644
--- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb
+++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb
@@ -28,7 +28,7 @@ class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase
end
end
- protected
+ private
def each_line_of_break_tests(&block)
lines = 0
max_test_lines = 0 # Don't limit below 21, because that's the header of the testfile
diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb
index e013bd578f..ebc9f92d23 100644
--- a/activesupport/test/multibyte_normalization_conformance_test.rb
+++ b/activesupport/test/multibyte_normalization_conformance_test.rb
@@ -83,7 +83,7 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase
end
end
- protected
+ private
def each_line_of_norm_tests(&block)
lines = 0
max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile
diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb
index 2201860d8a..a70516bb08 100644
--- a/activesupport/test/multibyte_test_helpers.rb
+++ b/activesupport/test/multibyte_test_helpers.rb
@@ -18,7 +18,7 @@ module MultibyteTestHelpers
end
UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd"
- CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance"
+ CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}"
FileUtils.mkdir_p(CACHE_DIR)
UNICODE_STRING = "こにちわ".freeze
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index a6f0d82e8a..11f743519f 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -273,7 +273,7 @@ module Notifications
assert !not_child.parent_of?(parent)
end
- protected
+ private
def random_id
@random_id ||= SecureRandom.hex(10)
end
diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb
index 4f58e6607a..a1d1c41dc2 100644
--- a/activesupport/test/number_helper_i18n_test.rb
+++ b/activesupport/test/number_helper_i18n_test.rb
@@ -1,5 +1,6 @@
require "abstract_unit"
require "active_support/number_helper"
+require "active_support/core_ext/hash/keys"
module ActiveSupport
class NumberHelperI18nTest < ActiveSupport::TestCase
diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb
index 67d8c4b0e3..bdd80307c7 100644
--- a/activesupport/test/reloader_test.rb
+++ b/activesupport/test/reloader_test.rb
@@ -2,12 +2,15 @@ require "abstract_unit"
class ReloaderTest < ActiveSupport::TestCase
def test_prepare_callback
- prepared = false
+ prepared = completed = false
reloader.to_prepare { prepared = true }
+ reloader.to_complete { completed = true }
assert !prepared
+ assert !completed
reloader.prepare!
assert prepared
+ assert !completed
prepared = false
reloader.wrap do
@@ -17,6 +20,15 @@ class ReloaderTest < ActiveSupport::TestCase
assert !prepared
end
+ def test_prepend_prepare_callback
+ i = 10
+ reloader.to_prepare { i += 1 }
+ reloader.to_prepare(prepend: true) { i = 0 }
+
+ reloader.prepare!
+ assert_equal 1, i
+ end
+
def test_only_run_when_check_passes
r = new_reloader { true }
invoked = false
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index e046b8d773..36c068b91f 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -175,6 +175,6 @@ class SafeBufferTest < ActiveSupport::TestCase
test "Should not affect frozen objects when accessing characters" do
x = "Hello".html_safe
- assert_equal x[/a/, 1], nil
+ assert_nil x[/a/, 1]
end
end
diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb
index 842bdd469d..e8f762da22 100644
--- a/activesupport/test/security_utils_test.rb
+++ b/activesupport/test/security_utils_test.rb
@@ -4,6 +4,11 @@ require "active_support/security_utils"
class SecurityUtilsTest < ActiveSupport::TestCase
def test_secure_compare_should_perform_string_comparison
assert ActiveSupport::SecurityUtils.secure_compare("a", "a")
- assert !ActiveSupport::SecurityUtils.secure_compare("a", "b")
+ 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")
end
end
diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb
index d41e4d6800..79a715349c 100644
--- a/activesupport/test/string_inquirer_test.rb
+++ b/activesupport/test/string_inquirer_test.rb
@@ -20,4 +20,25 @@ class StringInquirerTest < ActiveSupport::TestCase
def test_respond_to
assert_respond_to @string_inquirer, :development?
end
+
+ def test_respond_to_fallback_to_string_respond_to
+ String.class_eval do
+ def respond_to_missing?(name, include_private = false)
+ (name == :bar) || super
+ end
+ end
+ str = ActiveSupport::StringInquirer.new("hello")
+
+ assert_respond_to str, :are_you_ready?
+ assert_respond_to str, :bar
+ assert_not_respond_to str, :nope
+
+ ensure
+ String.class_eval do
+ undef_method :respond_to_missing?
+ def respond_to_missing?(name, include_private = false)
+ super
+ end
+ end
+ end
end
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index d769a8c145..af7fc44d66 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -257,7 +257,7 @@ class SetupAndTeardownTest < ActiveSupport::TestCase
def teardown
end
- protected
+ private
def reset_callback_record
@called_back = []
@@ -282,7 +282,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest
assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter)
end
- protected
+ private
def bar
@called_back << :bar
end
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
index c68be329bc..e0d3fb0cf5 100644
--- a/activesupport/test/time_travel_test.rb
+++ b/activesupport/test/time_travel_test.rb
@@ -3,6 +3,10 @@ require "active_support/core_ext/date_time"
require "active_support/core_ext/numeric/time"
class TimeTravelTest < ActiveSupport::TestCase
+ class TimeSubclass < ::Time; end
+ class DateSubclass < ::Date; end
+ class DateTimeSubclass < ::DateTime; end
+
def test_time_helper_travel
Time.stub(:now, Time.now) do
begin
@@ -90,11 +94,12 @@ class TimeTravelTest < ActiveSupport::TestCase
outer_expected_time = Time.new(2004, 11, 24, 01, 04, 44)
inner_expected_time = Time.new(2004, 10, 24, 01, 04, 44)
travel_to outer_expected_time do
- assert_raises(RuntimeError, /Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing./) do
+ e = assert_raises(RuntimeError) do
travel_to(inner_expected_time) do
#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)
end
end
end
@@ -142,4 +147,19 @@ class TimeTravelTest < ActiveSupport::TestCase
end
end
end
+
+ def test_time_helper_travel_with_time_subclass
+ assert_equal TimeSubclass, TimeSubclass.now.class
+ assert_equal DateSubclass, DateSubclass.today.class
+ assert_equal DateTimeSubclass, DateTimeSubclass.now.class
+
+ travel 1.day do
+ assert_equal TimeSubclass, TimeSubclass.now.class
+ assert_equal DateSubclass, DateSubclass.today.class
+ assert_equal DateTimeSubclass, DateTimeSubclass.now.class
+ assert_equal Time.now.to_s, TimeSubclass.now.to_s
+ assert_equal Date.today.to_s, DateSubclass.today.to_s
+ assert_equal DateTime.now.to_s, DateTimeSubclass.now.to_s
+ end
+ end
end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 914893ea10..de111cc40e 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -215,6 +215,95 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_equal secs, twz.to_f
end
+ def test_iso8601
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("1999-12-31T19:00:00")
+ assert_equal Time.utc(1999, 12, 31, 19), twz.time
+ assert_equal Time.utc(2000), twz.utc
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_iso8601_with_fractional_seconds
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("1999-12-31T19:00:00.750")
+ assert_equal 750000, twz.time.usec
+ assert_equal Time.utc(1999, 12, 31, 19, 0, 0 + Rational(3, 4)), twz.time
+ assert_equal Time.utc(2000, 1, 1, 0, 0, 0 + Rational(3, 4)), twz.utc
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_iso8601_with_zone
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("1999-12-31T14:00:00-10:00")
+ assert_equal Time.utc(1999, 12, 31, 19), twz.time
+ assert_equal Time.utc(2000), twz.utc
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_iso8601_with_invalid_string
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+
+ exception = assert_raises(ArgumentError) do
+ zone.iso8601("foobar")
+ end
+
+ assert_equal "invalid date", exception.message
+ end
+
+ def test_iso8601_with_missing_time_components
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("1999-12-31")
+ assert_equal Time.utc(1999, 12, 31, 0, 0, 0), twz.time
+ assert_equal Time.utc(1999, 12, 31, 5, 0, 0), twz.utc
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_iso8601_with_old_date
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("1883-12-31T19:00:00")
+ assert_equal [0, 0, 19, 31, 12, 1883], twz.to_a[0, 6]
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_iso8601_far_future_date_with_time_zone_offset_in_string
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("2050-12-31T19:00:00-10:00") # i.e., 2050-01-01 05:00:00 UTC
+ assert_equal [0, 0, 0, 1, 1, 2051], twz.to_a[0, 6]
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_iso8601_should_not_black_out_system_timezone_dst_jump
+ with_env_tz("EET") do
+ zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ twz = zone.iso8601("2012-03-25T03:29:00")
+ assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0, 6]
+ end
+ end
+
+ def test_iso8601_should_black_out_app_timezone_dst_jump
+ with_env_tz("EET") do
+ zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ twz = zone.iso8601("2012-03-11T02:29:00")
+ assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0, 6]
+ end
+ end
+
+ def test_iso8601_doesnt_use_local_dst
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["UTC"]
+ twz = zone.iso8601("2013-03-10T02:00:00")
+ assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time
+ end
+ end
+
+ def test_iso8601_handles_dst_jump
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("2013-03-10T02:00:00")
+ assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time
+ end
+ end
+
def test_parse
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
twz = zone.parse("1999-12-31 19:00:00")
@@ -314,6 +403,99 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
+ def test_rfc3339
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.rfc3339("1999-12-31T14:00:00-10:00")
+ assert_equal Time.utc(1999, 12, 31, 19), twz.time
+ assert_equal Time.utc(2000), twz.utc
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_rfc3339_with_fractional_seconds
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("1999-12-31T14:00:00.750-10:00")
+ assert_equal 750000, twz.time.usec
+ assert_equal Time.utc(1999, 12, 31, 19, 0, 0 + Rational(3, 4)), twz.time
+ assert_equal Time.utc(2000, 1, 1, 0, 0, 0 + Rational(3, 4)), twz.utc
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_rfc3339_with_missing_time
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+
+ exception = assert_raises(ArgumentError) do
+ zone.rfc3339("1999-12-31")
+ end
+
+ assert_equal "invalid date", exception.message
+ end
+
+ def test_rfc3339_with_missing_offset
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+
+ exception = assert_raises(ArgumentError) do
+ zone.rfc3339("1999-12-31T19:00:00")
+ end
+
+ assert_equal "invalid date", exception.message
+ end
+
+ def test_rfc3339_with_invalid_string
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+
+ exception = assert_raises(ArgumentError) do
+ zone.rfc3339("foobar")
+ end
+
+ assert_equal "invalid date", exception.message
+ end
+
+ def test_rfc3339_with_old_date
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.rfc3339("1883-12-31T19:00:00-05:00")
+ assert_equal [0, 0, 19, 31, 12, 1883], twz.to_a[0, 6]
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_rfc3339_far_future_date_with_time_zone_offset_in_string
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.rfc3339("2050-12-31T19:00:00-10:00") # i.e., 2050-01-01 05:00:00 UTC
+ assert_equal [0, 0, 0, 1, 1, 2051], twz.to_a[0, 6]
+ assert_equal zone, twz.time_zone
+ end
+
+ def test_rfc3339_should_not_black_out_system_timezone_dst_jump
+ with_env_tz("EET") do
+ zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ twz = zone.rfc3339("2012-03-25T03:29:00-07:00")
+ assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0, 6]
+ end
+ end
+
+ def test_rfc3339_should_black_out_app_timezone_dst_jump
+ with_env_tz("EET") do
+ zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ twz = zone.rfc3339("2012-03-11T02:29:00-08:00")
+ assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0, 6]
+ end
+ end
+
+ def test_rfc3339_doesnt_use_local_dst
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["UTC"]
+ twz = zone.rfc3339("2013-03-10T02:00:00Z")
+ assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time
+ end
+ end
+
+ def test_rfc3339_handles_dst_jump
+ with_env_tz "US/Eastern" do
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ twz = zone.iso8601("2013-03-10T02:00:00-05:00")
+ assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time
+ end
+ end
+
def test_strptime
zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
twz = zone.strptime("1999-12-31 12:00:00", "%Y-%m-%d %H:%M:%S")
@@ -416,7 +598,7 @@ class TimeZoneTest < ActiveSupport::TestCase
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)
- assert_equal nil, zone.instance_variable_get("@utc_offset")
+ assert_nil zone.instance_variable_get("@utc_offset")
assert_equal(-18_000, zone.utc_offset)
end
@@ -532,6 +714,10 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"]
end
+ def test_country_zones_without_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"]
+ end
+
def test_to_yaml
assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n", ActiveSupport::TimeZone["Hawaii"].to_yaml)
assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Europe/London\n", ActiveSupport::TimeZone["Europe/London"].to_yaml)
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index 040ddd25fc..466b69bcef 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -31,4 +31,22 @@ class TransliterateTest < ActiveSupport::TestCase
def test_transliterate_should_allow_a_custom_replacement_char
assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*")
end
+
+ def test_transliterate_handles_empty_string
+ assert_equal "", ActiveSupport::Inflector.transliterate("")
+ end
+
+ def test_transliterate_handles_nil
+ exception = assert_raises ArgumentError do
+ ActiveSupport::Inflector.transliterate(nil)
+ end
+ assert_equal "Can only transliterate strings. Received NilClass", exception.message
+ end
+
+ def test_transliterate_handles_unknown_object
+ exception = assert_raises ArgumentError do
+ ActiveSupport::Inflector.transliterate(Object.new)
+ end
+ assert_equal "Can only transliterate strings. Received Object", exception.message
+ end
end
diff --git a/ci/phantomjs.js b/ci/phantomjs.js
new file mode 100644
index 0000000000..7a33fb14a3
--- /dev/null
+++ b/ci/phantomjs.js
@@ -0,0 +1,149 @@
+/*
+ * 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/travis.rb b/ci/travis.rb
index c49a87d864..bb87c8f4ad 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -36,8 +36,10 @@ class Build
def run!(options = {})
self.options.update(options)
+
Dir.chdir(dir) do
announce(heading)
+
if guides?
run_bug_report_templates
else
@@ -69,7 +71,7 @@ class Build
end
tasks
else
- ["test", ("isolated" if isolated?), ("integration" if integration?)].compact.join(":")
+ ["test", ("isolated" if isolated?), ("integration" if integration?), ("ujs" if ujs?)].compact.join(":")
end
end
@@ -92,6 +94,10 @@ class Build
gem == "guides"
end
+ def ujs?
+ component.split(":").last == "ujs"
+ end
+
def isolated?
options[:isolated]
end
@@ -146,15 +152,16 @@ 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 gem == "railties" && isolated
next if gem == "ac" && isolated
next if gem == "ac:integration" && isolated
next if gem == "aj:integration" && isolated
next if gem == "guides" && isolated
+ next if gem == "av:ujs" && isolated
build = Build.new(gem, isolated: isolated)
results[build.key] = build.run!
-
end
end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index 2730d2dfea..d8b122d264 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,2 +1 @@
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/guides/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/Rakefile b/guides/Rakefile
index bf501f6a64..0a591558e1 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -1,23 +1,35 @@
namespace :guides do
-
desc 'Generate guides (for authors), use ONLY=foo to process just "foo.md"'
task generate: "generate:html"
- namespace :generate do
+ # Guides are written in UTF-8, but the environment may be configured for some
+ # other locale, these tasks are responsible for ensuring the default external
+ # encoding is UTF-8.
+ #
+ # Real use cases: Generation was reported to fail on a machine configured with
+ # GBK (Chinese). The docs server once got misconfigured somehow and had "C",
+ # which broke generation too.
+ task :encoding do
+ %w(LANG LANGUAGE LC_ALL).each do |env_var|
+ ENV[env_var] = "en_US.UTF-8"
+ end
+ end
+ namespace :generate do
desc "Generate HTML guides"
- task :html do
+ task :html => :encoding do
ENV["WARNINGS"] = "1" # authors can't disable this
ruby "rails_guides.rb"
end
desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/gp/feature.html?docId=1000765211"
- task :kindle do
- unless `kindlerb -v 2> /dev/null` =~ /kindlerb 1.0.1/
- abort "Please `gem install kindlerb` and make sure you have `kindlegen` in your PATH"
+ task :kindle => :encoding do
+ require "kindlerb"
+ unless Kindlerb.kindlegen_available?
+ abort "Please run `setupkindlerb` to install kindlegen"
end
unless `convert` =~ /convert/
- abort "Please install ImageMagick`"
+ abort "Please install ImageMagick"
end
ENV["KINDLE"] = "1"
Rake::Task["guides:generate:html"].invoke
@@ -26,13 +38,13 @@ namespace :guides do
# Validate guides -------------------------------------------------------------------------
desc 'Validate guides, use ONLY=foo to process just "foo.html"'
- task :validate do
+ task :validate => :encoding do
ruby "w3c_validator.rb"
end
desc "Show help"
task :help do
- puts <<-help
+ puts <<HELP
Guides are taken from the source directory, and the result goes into the
output directory. Assets are stored under files, and copied to output/files as
@@ -45,8 +57,9 @@ All of these processes are handled via rake tasks, here's a full list of them:
#{%x[rake -T]}
Some arguments may be passed via environment variables:
- WARNINGS=1
- Internal links (anchors) are checked, also detects duplicated IDs.
+ RAILS_VERSION=tag
+ If guides are being generated for a specific Rails version set the Git tag
+ here, otherwise the current SHA1 is going to be used to generate edge guides.
ALL=1
Force generation of all guides.
@@ -64,15 +77,12 @@ Some arguments may be passed via environment variables:
Use it when you want to generate translated guides in
source/<GUIDES_LANGUAGE> folder (such as source/es)
- EDGE=1
- Indicate generated guides should be marked as edge.
-
Examples:
- $ rake guides:generate ALL=1
- $ rake guides:generate EDGE=1
- $ rake guides:generate:kindle EDGE=1
+ $ rake guides:generate ALL=1 RAILS_VERSION=v5.1.0
+ $ rake guides:generate ONLY=migrations
+ $ rake guides:generate:kindle
$ rake guides:generate GUIDES_LANGUAGE=es
- help
+HELP
end
end
diff --git a/guides/assets/stylesheets/responsive-tables.css b/guides/assets/stylesheets/responsive-tables.css
index f5fbcbf948..f5fbcbf948 100755..100644
--- a/guides/assets/stylesheets/responsive-tables.css
+++ b/guides/assets/stylesheets/responsive-tables.css
diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb
index 960d269d90..46fabca3e8 100644
--- a/guides/bug_report_templates/action_controller_gem.rb
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -8,7 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
# Activate the gem you are reporting the issue against.
- gem "rails", "5.0.0"
+ gem "rails", "5.1.0.rc1"
end
require "rack/test"
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
index 486c7243ad..7644f6fe4a 100644
--- a/guides/bug_report_templates/action_controller_master.rb
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -8,6 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
gem "rails", github: "rails/rails"
+ gem "arel", github: "rails/arel"
end
require "action_controller/railtie"
diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb
index debc46ad54..71fe356ea0 100644
--- a/guides/bug_report_templates/active_job_gem.rb
+++ b/guides/bug_report_templates/active_job_gem.rb
@@ -8,7 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
# Activate the gem you are reporting the issue against.
- gem "activejob", "5.0.0"
+ gem "activejob", "5.1.0.rc1"
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 f61518713f..7591470440 100644
--- a/guides/bug_report_templates/active_job_master.rb
+++ b/guides/bug_report_templates/active_job_master.rb
@@ -8,6 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
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 e18302fe65..a685c257ea 100644
--- a/guides/bug_report_templates/active_record_gem.rb
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -8,7 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
# Activate the gem you are reporting the issue against.
- gem "activerecord", "5.0.0"
+ gem "activerecord", "5.1.0.rc1"
gem "sqlite3"
end
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
index 7265a671b0..8bbc1ef19e 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -8,6 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
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 ba80e6b4ad..b4e822dfe0 100644
--- a/guides/bug_report_templates/active_record_migrations_gem.rb
+++ b/guides/bug_report_templates/active_record_migrations_gem.rb
@@ -8,7 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
# Activate the gem you are reporting the issue against.
- gem "activerecord", "5.0.0.1"
+ gem "activerecord", "5.1.0.rc1"
gem "sqlite3"
end
diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb
index 13a375d1ba..84a4b71909 100644
--- a/guides/bug_report_templates/active_record_migrations_master.rb
+++ b/guides/bug_report_templates/active_record_migrations_master.rb
@@ -8,6 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
gem "rails", github: "rails/rails"
+ gem "arel", github: "rails/arel"
gem "sqlite3"
end
diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb
index a94848e25b..e1b705bea4 100644
--- a/guides/bug_report_templates/generic_gem.rb
+++ b/guides/bug_report_templates/generic_gem.rb
@@ -8,7 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
# Activate the gem you are reporting the issue against.
- gem "activesupport", "5.0.0"
+ gem "activesupport", "5.1.0.rc1"
end
require "active_support/core_ext/object/blank"
diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb
index d3a7ae4ac4..ed45726e92 100644
--- a/guides/bug_report_templates/generic_master.rb
+++ b/guides/bug_report_templates/generic_master.rb
@@ -8,6 +8,7 @@ end
gemfile(true) do
source "https://rubygems.org"
gem "rails", github: "rails/rails"
+ gem "arel", github: "rails/arel"
end
require "active_support"
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index 557d23f78c..0f611c8f2b 100644
--- a/guides/rails_guides.rb
+++ b/guides/rails_guides.rb
@@ -1,17 +1,27 @@
-pwd = File.dirname(__FILE__)
-$:.unshift pwd
+$:.unshift __dir__
-begin
- # Guides generation in the Rails repo.
- as_lib = File.join(pwd, "../activesupport/lib")
- ap_lib = File.join(pwd, "../actionpack/lib")
+as_lib = File.expand_path("../activesupport/lib", __dir__)
+ap_lib = File.expand_path("../actionpack/lib", __dir__)
+av_lib = File.expand_path("../actionview/lib", __dir__)
- $:.unshift as_lib if File.directory?(as_lib)
- $:.unshift ap_lib if File.directory?(ap_lib)
-rescue LoadError
- # Guides generation from gems.
- gem "actionpack", ">= 3.0"
-end
+$:.unshift as_lib if File.directory?(as_lib)
+$:.unshift ap_lib if File.directory?(ap_lib)
+$:.unshift av_lib if File.directory?(av_lib)
require "rails_guides/generator"
-RailsGuides::Generator.new.generate
+require "active_support/core_ext/object/blank"
+
+env_value = ->(name) { ENV[name].presence }
+env_flag = ->(name) { "1" == env_value[name] }
+
+version = env_value["RAILS_VERSION"]
+edge = `git rev-parse HEAD`.strip unless version
+
+RailsGuides::Generator.new(
+ edge: edge,
+ version: version,
+ all: env_flag["ALL"],
+ only: env_value["ONLY"],
+ kindle: env_flag["KINDLE"],
+ language: env_value["GUIDES_LANGUAGE"]
+).generate
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index a818ca9d72..28164a3cb4 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -1,54 +1,3 @@
-# ---------------------------------------------------------------------------
-#
-# This script generates the guides. It can be invoked via the
-# guides:generate rake task within the guides directory.
-#
-# Guides are taken from the source directory, and the resulting HTML goes into the
-# output directory. Assets are stored under files, and copied to output/files as
-# part of the generation process.
-#
-# Some arguments may be passed via environment variables:
-#
-# WARNINGS
-# If you are writing a guide, please work always with WARNINGS=1. Users can
-# generate the guides, and thus this flag is off by default.
-#
-# Internal links (anchors) are checked. If a reference is broken levenshtein
-# distance is used to suggest an existing one. This is useful since IDs are
-# generated by Markdown from headers and thus edits alter them.
-#
-# Also detects duplicated IDs. They happen if there are headers with the same
-# text. Please do resolve them, if any, so guides are valid XHTML.
-#
-# ALL
-# Set to "1" to force the generation of all guides.
-#
-# ONLY
-# Use ONLY if you want to generate only one or a set of guides. Prefixes are
-# enough:
-#
-# # generates only association_basics.html
-# ONLY=assoc rake guides:generate
-#
-# Separate many using commas:
-#
-# # generates only association_basics.html and command_line.html
-# ONLY=assoc,command rake guides:generate
-#
-# Note that if you are working on a guide generation will by default process
-# only that one, so ONLY is rarely used nowadays.
-#
-# GUIDES_LANGUAGE
-# Use GUIDES_LANGUAGE when you want to generate translated guides in
-# <tt>source/<GUIDES_LANGUAGE></tt> folder (such as <tt>source/es</tt>).
-# Ignore it when generating English guides.
-#
-# EDGE
-# Set to "1" to indicate generated guides should be marked as edge. This
-# inserts a badge and changes the preamble of the home page.
-#
-# ---------------------------------------------------------------------------
-
require "set"
require "fileutils"
@@ -64,46 +13,37 @@ require "rails_guides/levenshtein"
module RailsGuides
class Generator
- attr_reader :guides_dir, :source_dir, :output_dir, :edge, :warnings, :all
-
GUIDES_RE = /\.(?:erb|md)\z/
- def initialize(output = nil)
- set_flags_from_environment
+ def initialize(edge:, version:, all:, only:, kindle:, language:)
+ @edge = edge
+ @version = version
+ @all = all
+ @only = only
+ @kindle = kindle
+ @language = language
- if kindle?
+ if @kindle
check_for_kindlegen
register_kindle_mime_types
end
- initialize_dirs(output)
+ initialize_dirs
create_output_dir_if_needed
- end
-
- def set_flags_from_environment
- @edge = ENV["EDGE"] == "1"
- @warnings = ENV["WARNINGS"] == "1"
- @all = ENV["ALL"] == "1"
- @kindle = ENV["KINDLE"] == "1"
- @version = ENV["RAILS_VERSION"] || "local"
- @lang = ENV["GUIDES_LANGUAGE"]
- end
-
- def register_kindle_mime_types
- Mime::Type.register_alias("application/xml", :opf, %w(opf))
- Mime::Type.register_alias("application/xml", :ncx, %w(ncx))
+ initialize_markdown_renderer
end
def generate
generate_guides
copy_assets
- generate_mobi if kindle?
+ generate_mobi if @kindle
end
private
- def kindle?
- @kindle
+ def register_kindle_mime_types
+ Mime::Type.register_alias("application/xml", :opf, %w(opf))
+ Mime::Type.register_alias("application/xml", :ncx, %w(ncx))
end
def check_for_kindlegen
@@ -114,29 +54,35 @@ module RailsGuides
def generate_mobi
require "rails_guides/kindle"
- out = "#{output_dir}/kindlegen.out"
- Kindle.generate(output_dir, mobi, out)
+ out = "#{@output_dir}/kindlegen.out"
+ Kindle.generate(@output_dir, mobi, out)
puts "(kindlegen log at #{out})."
end
def mobi
- "ruby_on_rails_guides_#@version%s.mobi" % (@lang.present? ? ".#@lang" : "")
+ mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}"
+ mobi += ".#{@language}" if @language
+ mobi += ".mobi"
end
- def initialize_dirs(output)
- @guides_dir = File.join(File.dirname(__FILE__), "..")
- @source_dir = "#@guides_dir/source/#@lang"
- @output_dir = if output
- output
- elsif kindle?
- "#@guides_dir/output/kindle/#@lang"
- else
- "#@guides_dir/output/#@lang"
- end.sub(%r</$>, "")
+ def initialize_dirs
+ @guides_dir = File.expand_path("..", __dir__)
+
+ @source_dir = "#{@guides_dir}/source"
+ @source_dir += "/#{@language}" if @language
+
+ @output_dir = "#{@guides_dir}/output"
+ @output_dir += "/kindle" if @kindle
+ @source_dir += "/#{@language}" if @language
end
def create_output_dir_if_needed
- FileUtils.mkdir_p(output_dir)
+ FileUtils.mkdir_p(@output_dir)
+ end
+
+ def initialize_markdown_renderer
+ Markdown::Renderer.edge = @edge
+ Markdown::Renderer.version = @version
end
def generate_guides
@@ -147,27 +93,27 @@ module RailsGuides
end
def guides_to_generate
- guides = Dir.entries(source_dir).grep(GUIDES_RE)
+ guides = Dir.entries(@source_dir).grep(GUIDES_RE)
- if kindle?
- Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry|
+ if @kindle
+ Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry|
next if entry == "KINDLE.md"
guides << "kindle/#{entry}"
end
end
- ENV.key?("ONLY") ? select_only(guides) : guides
+ @only ? select_only(guides) : guides
end
def select_only(guides)
- prefixes = ENV["ONLY"].split(",").map(&:strip)
+ prefixes = @only.split(",").map(&:strip)
guides.select do |guide|
- guide.start_with?("kindle".freeze, *prefixes)
+ guide.start_with?("kindle", *prefixes)
end
end
def copy_assets
- FileUtils.cp_r(Dir.glob("#{guides_dir}/assets/*"), output_dir)
+ FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir)
end
def output_file_for(guide)
@@ -179,22 +125,28 @@ module RailsGuides
end
def output_path_for(output_file)
- File.join(output_dir, File.basename(output_file))
+ File.join(@output_dir, File.basename(output_file))
end
def generate?(source_file, output_file)
- fin = File.join(source_dir, source_file)
+ fin = File.join(@source_dir, source_file)
fout = output_path_for(output_file)
- all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
+ @all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
end
def generate_guide(guide, output_file)
output_path = output_path_for(output_file)
puts "Generating #{guide} as #{output_file}"
- layout = kindle? ? "kindle/layout" : "layout"
+ 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}", lang: @lang)
+ view = ActionView::Base.new(
+ @source_dir,
+ edge: @edge,
+ version: @version,
+ mobi: "kindle/#{mobi}",
+ language: @language
+ )
view.extend(Helpers)
if guide =~ /\.(\w+)\.erb$/
@@ -202,10 +154,15 @@ module RailsGuides
# 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(File.join(source_dir, guide))
- result = RailsGuides::Markdown.new(view, layout).render(body)
-
- warn_about_broken_links(result) if @warnings
+ 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
f.write(result)
@@ -231,7 +188,7 @@ module RailsGuides
# Footnotes.
anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten)
anchors += Set.new(html.scan(/<sup\s+class="footnote"\s+id="([^"]+)/).flatten)
- return anchors
+ anchors
end
def check_fragment_identifiers(html, anchors)
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
index 4b73ae9518..9536d0bd3b 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -1,9 +1,6 @@
#!/usr/bin/env ruby
-unless `which kindlerb`
- abort "Please gem install kindlerb"
-end
-
+require "kindlerb"
require "nokogiri"
require "fileutils"
require "yaml"
@@ -28,10 +25,9 @@ module Kindle
generate_document_metadata(mobi_outfile)
puts "Creating MOBI document with kindlegen. This may take a while."
- cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1"
- puts cmd
- system(cmd)
- puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}"
+ if Kindlerb.run(output_dir)
+ puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}"
+ end
end
end
diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb
index 009e5aff99..bf2cc82c7c 100644
--- a/guides/rails_guides/markdown.rb
+++ b/guides/rails_guides/markdown.rb
@@ -4,12 +4,14 @@ require "rails_guides/markdown/renderer"
module RailsGuides
class Markdown
- def initialize(view, layout)
- @view = view
- @layout = layout
+ def initialize(view:, layout:, edge:, version:)
+ @view = view
+ @layout = layout
+ @edge = edge
+ @version = version
@index_counter = Hash.new(0)
- @raw_header = ""
- @node_ids = {}
+ @raw_header = ""
+ @node_ids = {}
end
def render(body)
@@ -60,7 +62,8 @@ module RailsGuides
autolink: true,
strikethrough: true,
superscript: true,
- tables: true)
+ tables: true
+ )
end
def extract_raw_header_and_body
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
index deab741023..20cbd568c9 100644
--- a/guides/rails_guides/markdown/renderer.rb
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -1,9 +1,7 @@
module RailsGuides
class Markdown
class Renderer < Redcarpet::Render::HTML
- def initialize(options = {})
- super
- end
+ cattr_accessor :edge, :version
def block_code(code, language)
<<-HTML
@@ -15,6 +13,16 @@ module RailsGuides
HTML
end
+ def link(url, title, content)
+ if url.start_with?("http://api.rubyonrails.org")
+ %(<a href="#{api_link(url)}">#{content}</a>)
+ elsif title
+ %(<a href="#{url}" title="#{title}">#{content}</a>)
+ else
+ %(<a href="#{url}">#{content}</a>)
+ end
+ end
+
def header(text, header_level)
# Always increase the heading level by 1, so we can use h1, h2 heading in the document
header_level += 1
@@ -23,7 +31,9 @@ HTML
end
def paragraph(text)
- if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/
+ if text =~ %r{^NOTE:\s+Defined\s+in\s+<code>(.*?)</code>\.?$}
+ %(<div class="note"><p>Defined in <code><a href="#{github_file_url($1)}">#{$1}</a></code>.</p></div>)
+ elsif text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/
convert_notes(text)
elsif text.include?("DO NOT READ THIS FILE ON GITHUB")
elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/
@@ -79,6 +89,33 @@ HTML
%(<div class="#{css_class}"><p>#{$2.strip}</p></div>)
end
end
+
+ def github_file_url(file_path)
+ tree = version || edge
+
+ root = file_path[%r{(.+)/}, 1]
+ path = case root
+ when "abstract_controller", "action_controller", "action_dispatch"
+ "actionpack/lib/#{file_path}"
+ when /\A(action|active)_/
+ "#{root.sub("_", "")}/lib/#{file_path}"
+ else
+ file_path
+ end
+
+
+ "https://github.com/rails/rails/tree/#{tree}/#{path}"
+ end
+
+ def api_link(url)
+ if url =~ %r{http://api\.rubyonrails\.org/v\d+\.}
+ url
+ elsif edge
+ url.sub("api", "edgeapi")
+ else
+ url.sub(/(?<=\.org)/, "/#{version}")
+ end
+ end
end
end
end
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index a98f7be067..5f4be07351 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_0_release_notes.md
@@ -150,7 +150,7 @@ The type of an attribute is given the opportunity to change how dirty
tracking is performed.
See its
-[documentation](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html)
+[documentation](http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html)
for a detailed write up.
@@ -242,7 +242,7 @@ Please refer to the [Changelog][railties] for detailed changes.
[Pull Request](https://github.com/rails/rails/pull/22288))
* New applications are generated with the evented file system monitor enabled
- on Linux and Mac OS X. The feature can be opted out by passing
+ on Linux and macOS. The feature can be opted out by passing
`--skip-listen` to the generator.
([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003),
[commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202))
@@ -499,6 +499,9 @@ Please refer to the [Changelog][action-view] for detailed changes.
`datetime-local`.
([Pull Request](https://github.com/rails/rails/pull/25469))
+* Allow blocks while rendering with the `render partial:` helper.
+ ([Pull Request](https://github.com/rails/rails/pull/17974))
+
Action Mailer
-------------
@@ -585,7 +588,7 @@ Please refer to the [Changelog][active-record] for detailed changes.
gem. ([Pull Request](https://github.com/rails/rails/pull/21161))
* Removed support for the legacy `mysql` database adapter from core. Most users should
- be able to use `mysql2`. It will be converted to a separate gem when when we find someone
+ be able to use `mysql2`. It will be converted to a separate gem when we find someone
to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642),
[Pull Request 2](https://github.com/rails/rails/pull/22715))
diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md
new file mode 100644
index 0000000000..5d4885d55c
--- /dev/null
+++ b/guides/source/5_1_release_notes.md
@@ -0,0 +1,277 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
+
+Ruby on Rails 5.1 Release Notes
+===============================
+
+Highlights in Rails 5.1:
+
+* Yarn Support
+* Optional Webpack support
+* jQuery no longer a default dependency
+* System tests
+* Encrypted secrets
+* Parameterized mailers
+* Direct & resolved routes
+* Unification of form_for and form_tag into form_with
+
+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-1-stable) in the main Rails
+repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 5.1
+----------------------
+
+ToDo
+
+Major Features
+--------------
+
+### Yarn Support
+
+[Pull Request](https://github.com/rails/rails/pull/26836)
+
+Rails 5.1 will allow managing JavaScript dependencies
+from NPM via Yarn. This will make it easy to use libraries like React, VueJS
+or any other library from NPM world. The Yarn support is integrated with
+the asset pipeline so that all dependencies will work seamlessly with the
+Rails 5.1 app.
+
+### Optional Webpack support
+
+[Pull Request](https://github.com/rails/rails/pull/27288)
+
+Rails apps can integrate with [Webpack](https://webpack.js.org/), a JavaScript
+asset bundler, more easily using the new [Webpacker](https://github.com/rails/webpacker)
+gem. Use the `--webpack` flag when generating new applications to enable Webpack
+integration.
+
+This is fully compatible with the asset pipeline, which you can continue to use for
+images, fonts, sounds, and other assets. You can even have some JavaScript code
+managed by the asset pipeline, and other code processed via Webpack. All of this is managed
+by Yarn, which is enabled by default.
+
+### jQuery no longer a default dependency
+
+[Pull Request](https://github.com/rails/rails/pull/27113)
+
+jQuery was required by default in earlier versions of Rails to provide features
+like `data-remote`, `data-confirm` and other parts of Rails' Unobtrusive JavaScript
+offerings. It is no longer required, as the UJS has been rewritten to use plain,
+vanilla JavaScript. This code now ships inside of Action View as
+`rails-ujs`.
+
+You can still use the jQuery version if needed, but it is no longer required by default.
+
+### System tests
+
+[Pull Request](https://github.com/rails/rails/pull/26703)
+
+Rails 5.1 has baked-in support for writing Capybara tests, in the form of
+System tests. You need no longer worry about configuring Capybara and
+database cleaning strategies for such tests. Rails 5.1 provides a wrapper
+for running tests in Chrome with additional features such as failure
+screenshots.
+
+### Encrypted secrets
+
+[Pull Request](https://github.com/rails/rails/pull/28038)
+
+Rails will now allow management of application secrets in a secure way,
+building on top of the [sekrets](https://github.com/ahoward/sekrets) gem.
+
+Run `bin/rails secrets:setup` to setup a new encrypted secrets file. This will
+also generate a master key, which must be stored outside of the repository. The
+secrets themselves can then be safely checked into the revision control system,
+in an encrypted form.
+
+Secrets will be decrypted in production, using a key stored either in the
+`RAILS_MASTER_KEY` environment variable, or in a key file.
+
+### Parameterized mailers
+
+[Pull Request](https://github.com/rails/rails/pull/27825)
+
+Allows specifying common params used for all methods in a mailer class
+to share instance variables, headers and other common setup.
+
+``` ruby
+class InvitationsMailer < ApplicationMailer
+
+ before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
+ before_action { @account = params[:inviter].account }
+
+ 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
+
+InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
+```
+
+### Direct & resolved routes
+
+[Pull Request](https://github.com/rails/rails/pull/23138)
+
+Rails 5.1 has added two new methods, `resolve` and `direct`, to the routing
+DSL.
+
+The `resolve` method allows customizing polymorphic mapping of models.
+
+``` 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`.
+
+The `direct` method allows creation of custom URL helpers.
+
+``` ruby
+direct(:homepage) { "http://www.rubyonrails.org" }
+
+>> 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
+```
+
+### Unification of form_for and form_tag into form_with
+
+[Pull Request](https://github.com/rails/rails/pull/26976)
+
+Before Rails 5.1, there were two interfaces for handling HTML forms:
+`form_for` for model instances and `form_tag` for custom URLs.
+
+Rails 5.1 combines both of these interfaces with `form_with`, and
+can generate form tags based on URLs, scopes or models.
+
+``` erb
+# Using just a URL:
+
+<%= form_with url: posts_path do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+
+# =>
+<form action="/posts" method="post" data-remote="true">
+ <input type="text" name="title">
+</form>
+
+# Adding a scope prefixes the input field names:
+
+<%= form_with scope: :post, url: posts_path do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+# =>
+<form action="/posts" method="post" data-remote="true">
+ <input type="text" name="post[title]">
+</form>
+
+# Using a model infers both the URL and scope:
+
+<%= form_with model: Post.new do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+# =>
+<form action="/posts" method="post" data-remote="true">
+ <input type="text" name="post[title]">
+</form>
+
+# An existing model makes an update form and fills out field values:
+
+<%= form_with model: Post.first do |form| %>
+ <%= form.text_field :title %>
+<% end %>
+# =>
+<form action="/posts/1" method="post" data-remote="true">
+ <input type="hidden" name="_method" value="patch">
+ <input type="text" name="post[title]" value="<the title of the post>">
+</form>
+```
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+Action View
+-------------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+Active Job
+-----------
+
+Please refer to the [Changelog][active-job] for detailed changes.
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+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-1-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md
+[action-cable]: https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md
+[active-job]: https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index f50bcddbe7..8afec00018 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -1,8 +1,8 @@
-<h2>Ruby on Rails Guides (<%= @edge ? @version[0, 7] : @version %>)</h2>
+<h2>Ruby on Rails Guides (<%= @edge ? @edge[0, 7] : @version %>)</h2>
<% if @edge %>
<p>
- These are <b>Edge Guides</b>, based on the current <a href="https://github.com/rails/rails/tree/<%= @version %>">master</a> branch.
+ These are <b>Edge Guides</b>, based on <a href="https://github.com/rails/rails/tree/<%= @edge %>">master@<%= @edge[0, 7] %></a>.
</p>
<p>
If you are looking for the ones for the stable version, please check
@@ -10,12 +10,13 @@
</p>
<% else %>
<p>
- These are the new guides for Rails 5.0 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
+ These are the new guides for Rails 5.1 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.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>,
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index 3716aa0ecb..50a28571b4 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -6,7 +6,7 @@ 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 on backend and frontend
* How to setup Action Cable
* How to setup channels
* Deployment and Architecture setup for running Action Cable
@@ -62,10 +62,10 @@ module ApplicationCable
self.current_user = find_verified_user
end
- protected
+ private
def find_verified_user
- if current_user = User.find_by(id: cookies.signed[:user_id])
- current_user
+ if verified_user = User.find_by(id: cookies.signed[:user_id])
+ verified_user
else
reject_unauthorized_connection
end
@@ -240,8 +240,8 @@ WebNotificationsChannel.broadcast_to(
```
The `WebNotificationsChannel.broadcast_to` call places a message in the current
-subscription adapter (by default `redis` for production and `async` for development and
-test environments)'s pubsub queue under a separate broadcasting name for each user.
+subscription adapter (by default `redis` for production and `async` for development and
+test environments)'s pubsub queue under a separate broadcasting name for each user.
For a user with an ID of 1, the broadcasting name would be `web_notifications:1`.
The channel has been instructed to stream everything that arrives at
@@ -530,7 +530,7 @@ Action Cable has two required configurations: a subscription adapter and allowed
### Subscription Adapter
By default, Action Cable looks for a configuration file in `config/cable.yml`.
-The file must specify an adapter and a URL for each Rails environment. See the
+The file must specify an adapter for each Rails environment. See the
[Dependencies](#dependencies) section for additional information on adapters.
```yaml
@@ -543,7 +543,28 @@ test:
production:
adapter: redis
url: redis://10.10.3.153:6381
+ channel_prefix: appname_production
```
+#### Adapter Configuration
+
+Below is a list of the subscription adapters available for end users.
+
+##### Async Adapter
+
+The async adapter is intended for development/testing and should not be used in production.
+
+##### 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
+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
+
+The PostgreSQL adapter uses Active Record's connection pool, and thus the
+application's `config/database.yml` database configuration, for its connection.
+This may change in the future. [#27214](https://github.com/rails/rails/issues/27214)
### Allowed Request Origins
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 40eb838d32..69c4a00c5f 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -61,7 +61,7 @@ end
The [Layouts & Rendering Guide](layouts_and_rendering.html) explains this in more detail.
-`ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the API documentation or in the source itself.
+`ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the [API documentation](http://api.rubyonrails.org/classes/ActionController.html) or in the source itself.
Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with `private` or `protected`) which are not intended to be actions, like auxiliary methods or filters.
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index 34847832fd..9673571909 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -160,8 +160,8 @@ When you call the `mail` method now, Action Mailer will detect the two templates
#### Calling the Mailer
Mailers are really just another way to render a view. Instead of rendering a
-view and sending out the HTTP protocol, they are just sending it out through the
-email protocols instead. Due to this, it makes sense to just have your
+view and sending it over the HTTP protocol, they are just sending it out through
+the email protocols instead. Due to this, it makes sense to just have your
controller tell the Mailer to send an email when a user is successfully created.
Setting this up is painfully simple.
@@ -400,7 +400,7 @@ class UserMailer < ApplicationMailer
mail(to: @user.email,
subject: 'Welcome to My Awesome Site') do |format|
format.html { render 'another_template' }
- format.text { render text: 'Render text' }
+ format.text { render plain: 'Render text' }
end
end
end
@@ -525,7 +525,7 @@ By using the full URL, your links will now work in your emails.
#### Generating URLs with `url_for`
-`url_for` generate full URL by default in templates.
+`url_for` generates a full URL by default in templates.
If you did not configure the `:host` option globally make sure to pass it to
`url_for`.
@@ -550,8 +550,9 @@ url helper.
<%= user_url(@user, host: 'example.com') %>
```
-NOTE: non-`GET` links require [jQuery UJS](https://github.com/rails/jquery-ujs)
-and won't work in mailer templates. They will result in normal `GET` requests.
+NOTE: non-`GET` links require [rails-ujs](https://github.com/rails/rails-ujs) or
+[jQuery UJS](https://github.com/rails/jquery-ujs), and won't work in mailer templates.
+They will result in normal `GET` requests.
### Adding images in Action Mailer Views
@@ -574,7 +575,7 @@ Now you can display an image inside your email.
### Sending Multipart Emails
Action Mailer will automatically send multipart emails if you have different
-templates for the same action. So, for our UserMailer example, if you have
+templates for the same action. So, for our `UserMailer` example, if you have
`welcome_email.text.erb` and `welcome_email.html.erb` in
`app/views/user_mailer`, Action Mailer will automatically send a multipart email
with the HTML and text versions setup as different parts.
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index c65d1e6de5..b58ca61848 100644
--- a/guides/source/active_job_basics.md
+++ b/guides/source/active_job_basics.md
@@ -114,7 +114,7 @@ For enqueuing and executing jobs in production you need to set up a queuing back
that is to say you need to decide for a 3rd-party queuing library that Rails should use.
Rails itself only provides an in-process queuing system, which only keeps the jobs in RAM.
If the process crashes or the machine is reset, then all outstanding jobs are lost with the
-default async back-end. This may be fine for smaller apps or non-critical jobs, but most
+default async backend. This may be fine for smaller apps or non-critical jobs, but most
production apps will need to pick a persistent backend.
### Backends
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index 732e553c62..e26805d22c 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -87,7 +87,7 @@ end
### Conversion
If a class defines `persisted?` and `id` methods, then you can include the
-`ActiveModel::Conversion` module in that class and call the Rails conversion
+`ActiveModel::Conversion` module in that class, and call the Rails conversion
methods on objects of that class.
```ruby
@@ -156,16 +156,17 @@ person.changed? # => false
person.first_name = "First Name"
person.first_name # => "First Name"
-# returns true if any of the attributes have unsaved changes, false otherwise.
+# returns true if any of the attributes have unsaved changes.
person.changed? # => true
# returns a list of attributes that have changed before saving.
person.changed # => ["first_name"]
-# returns a hash of the attributes that have changed with their original values.
+# returns a Hash of the attributes that have changed with their original values.
person.changed_attributes # => {"first_name"=>nil}
-# returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field.
+# returns a Hash of changes, with the attribute names as the keys, and the
+# values as an array of the old and new values for that field.
person.changes # => {"first_name"=>[nil, "First Name"]}
```
@@ -179,7 +180,7 @@ person.first_name # => "First Name"
person.first_name_changed? # => true
```
-Track what was the previous value of the attribute.
+Track the previous value of the attribute.
```ruby
# attr_name_was accessor
@@ -187,7 +188,7 @@ person.first_name_was # => nil
```
Track both previous and current value of the changed attribute. Returns an array
-if changed, else returns nil.
+if changed, otherwise returns nil.
```ruby
# attr_name_change
@@ -197,7 +198,7 @@ person.last_name_change # => nil
### Validations
-The `ActiveModel::Validations` module adds the ability to validate class objects
+The `ActiveModel::Validations` module adds the ability to validate objects
like in Active Record.
```ruby
@@ -225,7 +226,7 @@ person.valid? # => raises ActiveModel::StrictValidationFa
### Naming
-`ActiveModel::Naming` adds a number of class methods which make the naming and routing
+`ActiveModel::Naming` adds a number of class methods which make naming and routing
easier to manage. The module defines the `model_name` class method which
will define a number of accessors using some `ActiveSupport::Inflector` methods.
@@ -248,7 +249,7 @@ Person.model_name.singular_route_key # => "person"
### Model
-`ActiveModel::Model` adds the ability to a class to work with Action Pack and
+`ActiveModel::Model` adds the ability for a class to work with Action Pack and
Action View right out of the box.
```ruby
@@ -293,7 +294,7 @@ objects.
### Serialization
`ActiveModel::Serialization` provides basic serialization for your object.
-You need to declare an attributes hash which contains the attributes you want to
+You need to declare an attributes Hash which contains the attributes you want to
serialize. Attributes must be strings, not symbols.
```ruby
@@ -308,7 +309,7 @@ class Person
end
```
-Now you can access a serialized hash of your object using the `serializable_hash`.
+Now you can access a serialized Hash of your object using the `serializable_hash` method.
```ruby
person = Person.new
@@ -319,13 +320,14 @@ person.serializable_hash # => {"name"=>"Bob"}
#### ActiveModel::Serializers
-Rails provides an `ActiveModel::Serializers::JSON` serializer.
-This module automatically include the `ActiveModel::Serialization`.
+Active Model also provides the `ActiveModel::Serializers::JSON` module
+for JSON serializing / deserializing. This module automatically includes the
+previously discussed `ActiveModel::Serialization` module.
##### ActiveModel::Serializers::JSON
-To use the `ActiveModel::Serializers::JSON` you only need to change from
-`ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`.
+To use `ActiveModel::Serializers::JSON` you only need to change the
+module you are including from `ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`.
```ruby
class Person
@@ -339,7 +341,8 @@ class Person
end
```
-With the `as_json` method you have a hash representing the model.
+The `as_json` method, similar to `serializable_hash`, provides a Hash representing
+the model.
```ruby
person = Person.new
@@ -348,8 +351,8 @@ person.name = "Bob"
person.as_json # => {"name"=>"Bob"}
```
-From a JSON string you define the attributes of the model.
-You need to have the `attributes=` method defined on your class:
+You can also define the attributes for a model from a JSON string.
+However, you need to define the `attributes=` method on your class:
```ruby
class Person
@@ -369,7 +372,7 @@ class Person
end
```
-Now it is possible to create an instance of person and set the attributes using `from_json`.
+Now it is possible to create an instance of `Person` and set attributes using `from_json`.
```ruby
json = { name: 'Bob' }.to_json
@@ -389,8 +392,8 @@ class Person
end
```
-With the `human_attribute_name` you can transform attribute names into a more
-human format. The human format is defined in your locale file.
+With the `human_attribute_name` method, you can transform attribute names into a
+more human-readable format. The human-readable format is defined in your locale file(s).
* config/locales/app.pt-BR.yml
@@ -411,7 +414,7 @@ Person.human_attribute_name('name') # => "Nome"
`ActiveModel::Lint::Tests` allows you to test whether an object is compliant with
the Active Model API.
-* app/models/person.rb
+* `app/models/person.rb`
```ruby
class Person
@@ -419,7 +422,7 @@ the Active Model API.
end
```
-* test/models/person_test.rb
+* `test/models/person_test.rb`
```ruby
require 'test_helper'
@@ -454,9 +457,9 @@ features out of the box.
### SecurePassword
`ActiveModel::SecurePassword` provides a way to securely store any
-password in an encrypted form. On including this module, a
+password in an encrypted form. When you include this module, a
`has_secure_password` class method is provided which defines
-an accessor named `password` with certain validations on it.
+a `password` accessor with certain validations on it.
#### Requirements
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 2a1c960887..77bd3c97e8 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -36,7 +36,7 @@ class User < ApplicationRecord
before_validation :ensure_login_has_a_value
- protected
+ private
def ensure_login_has_a_value
if login.nil?
self.login = email unless email.blank?
@@ -66,7 +66,7 @@ class User < ApplicationRecord
# :on takes an array as well
after_validation :set_location, on: [ :create, :update ]
- protected
+ private
def normalize_name
self.name = name.downcase.titleize
end
@@ -77,7 +77,7 @@ class User < ApplicationRecord
end
```
-It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
+It is considered good practice to declare callback methods as private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
Available Callbacks
-------------------
@@ -202,11 +202,9 @@ The following methods trigger callbacks:
* `create`
* `create!`
-* `decrement!`
* `destroy`
* `destroy!`
* `destroy_all`
-* `increment!`
* `save`
* `save!`
* `save(validate: false)`
@@ -290,7 +288,7 @@ Article destroyed
Conditional Callbacks
---------------------
-As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option.
+As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option.
### Using `:if` and `:unless` with a `Symbol`
@@ -302,16 +300,6 @@ class Order < ApplicationRecord
end
```
-### Using `:if` and `:unless` with a String
-
-You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition:
-
-```ruby
-class Order < ApplicationRecord
- before_save :normalize_card_number, if: "paid_with_card?"
-end
-```
-
### Using `:if` and `:unless` with a `Proc`
Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners:
diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md
index d7e35490ef..6d07291b07 100644
--- a/guides/source/active_record_postgresql.md
+++ b/guides/source/active_record_postgresql.md
@@ -111,7 +111,7 @@ profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
Profile.where("settings->'color' = ?", "yellow")
-#=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
+# => #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
```
### JSON
@@ -422,7 +422,7 @@ device = Device.create
device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e"
```
-NOTE: `uuid_generate_v4()` (from `uuid-ossp`) is assumed if no `:default` option was
+NOTE: `gen_random_uuid()` (from `pgcrypto`) is assumed if no `:default` option was
passed to `create_table`.
Full Text Search
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 31220f9be2..2902c5d677 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -953,9 +953,6 @@ class Client < ApplicationRecord
end
```
-NOTE: Please note that the optimistic locking will be ignored if you update the
-locking column's value.
-
### Pessimistic Locking
Pessimistic locking uses a locking mechanism provided by the underlying database. Using `lock` when building a relation obtains an exclusive lock on the selected rows. Relations using `lock` are usually wrapped inside a transaction for preventing deadlock conditions.
@@ -1547,7 +1544,7 @@ SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
ON comments.person_id = people.id
-WHERE comments.created_at = '2015-01-01'
+WHERE comments.created_at > '2015-01-01'
```
### Retrieving specific data from multiple tables
@@ -1871,7 +1868,7 @@ Which will execute:
```sql
SELECT count(DISTINCT clients.id) AS count_all FROM clients
- LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
+ LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE
(clients.first_name = 'Ryan' AND orders.status = 'received')
```
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index 665e97c470..5313361dfd 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -490,9 +490,6 @@ If you set `:only_integer` to `true`, then it will use the
regular expression to validate the attribute's value. Otherwise, it will try to
convert the value to a number using `Float`.
-WARNING. Note that the regular expression above allows a trailing newline
-character.
-
```ruby
class Player < ApplicationRecord
validates :points, numericality: true
@@ -916,18 +913,6 @@ class Order < ApplicationRecord
end
```
-### Using a String with `:if` and `:unless`
-
-You can also use a string that will be evaluated using `eval` and needs to
-contain valid Ruby code. You should use this option only when the string
-represents a really short condition.
-
-```ruby
-class Person < ApplicationRecord
- validates :surname, presence: true, if: "name.nil?"
-end
-```
-
### Using a Proc with `:if` and `:unless`
Finally, it's possible to associate `:if` and `:unless` with a `Proc` object
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 6bbc79a326..67bed4c8da 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -135,36 +135,53 @@ NOTE: Defined in `active_support/core_ext/object/blank.rb`.
### `duplicable?`
-A few fundamental objects in Ruby are singletons. For example, in the whole life of a program the integer 1 refers always to the same instance:
+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.
```ruby
-1.object_id # => 3
-Math.cos(0).to_i.object_id # => 3
+"foo".dup # => "foo"
+"".dup # => ""
+1.method(:+).dup # => TypeError: allocator undefined for Method
+Complex(0).dup # => TypeError: can't copy Complex
```
-Hence, there's no way these objects can be duplicated through `dup` or `clone`:
+Active Support provides `duplicable?` to query an object about this:
```ruby
-true.dup # => TypeError: can't dup TrueClass
+"foo".duplicable? # => true
+"".duplicable? # => true
+Rational(1).duplicable? # => false
+Complex(1).duplicable? # => false
+1.method(:+).duplicable? # => false
```
-Some numbers which are not singletons are not duplicable either:
+`duplicable?` matches Ruby's `dup` according to the Ruby version.
+
+So in 2.4:
```ruby
-0.0.clone # => allocator undefined for Float
-(2**1024).clone # => allocator undefined for Bignum
+nil.dup # => nil
+:my_symbol.dup # => :my_symbol
+1.dup # => 1
+
+nil.duplicable? # => true
+:my_symbol.duplicable? # => true
+1.duplicable? # => true
```
-Active Support provides `duplicable?` to programmatically query an object about this property:
+Whereas in 2.2 and 2.3:
```ruby
-"foo".duplicable? # => true
-"".duplicable? # => true
-0.0.duplicable? # => false
-false.duplicable? # => false
-```
+nil.dup # => TypeError: can't dup NilClass
+:my_symbol.dup # => TypeError: can't dup Symbol
+1.dup # => TypeError: can't dup Fixnum
-By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, module, and method objects.
+nil.duplicable? # => false
+:my_symbol.duplicable? # => false
+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.
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 25717e04e4..61b7112247 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -78,9 +78,9 @@ requests can mean faster loading for your application.
Sprockets concatenates all JavaScript files into one master `.js` file and all
CSS files into one master `.css` file. As you'll learn later in this guide, you
can customize this strategy to group files any way you like. In production,
-Rails inserts an MD5 fingerprint into each filename so that the file is cached
-by the web browser. You can invalidate the cache by altering this fingerprint,
-which happens automatically whenever you change the file contents.
+Rails inserts an SHA256 fingerprint into each filename so that the file is
+cached by the web browser. You can invalidate the cache by altering this
+fingerprint, which happens automatically whenever you change the file contents.
The second feature of the asset pipeline is asset minification or compression.
For CSS files, this is done by removing whitespace and comments. For JavaScript,
@@ -106,7 +106,7 @@ or in web browsers) to keep their own copy of the content. When the content is
updated, the fingerprint will change. This will cause the remote clients to
request a new copy of the content. This is generally known as _cache busting_.
-The technique sprockets uses for fingerprinting is to insert a hash of the
+The technique Sprockets uses for fingerprinting is to insert a hash of the
content into the name, usually at the end. For example a CSS file `global.css`
```
@@ -207,7 +207,7 @@ default .coffee and .scss files will not be precompiled on their own. See
precompiling works.
NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript.
-If you are using Mac OS X or Windows, you have a JavaScript runtime installed in
+If you are using macOS or Windows, you have a JavaScript runtime installed in
your operating system. Check [ExecJS](https://github.com/rails/execjs#readme) documentation to know all supported JavaScript runtimes.
You can also disable generation of controller specific asset files by adding the
@@ -335,7 +335,7 @@ an asset has been updated and if so loads it into the page:
<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
```
-In regular views you can access images in the `public/assets/images` directory
+In regular views you can access images in the `app/assets/images` directory
like this:
```erb
@@ -346,9 +346,9 @@ Provided that the pipeline is enabled within your application (and not disabled
in the current environment context), this file is served by Sprockets. If a file
exists at `public/assets/rails.png` it is served by the web server.
-Alternatively, a request for a file with an MD5 hash such as
-`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same
-way. How these hashes are generated is covered in the [In
+Alternatively, a request for a file with an SHA256 hash such as
+`public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png`
+is treated the same way. How these hashes are generated is covered in the [In
Production](#in-production) section later on in this guide.
Sprockets will also look through the paths specified in `config.assets.paths`,
@@ -654,7 +654,7 @@ In the production environment Sprockets uses the fingerprinting scheme outlined
above. By default Rails assumes assets have been precompiled and will be
served as static assets by your web server.
-During the precompilation phase an MD5 is generated from the contents of the
+During the precompilation phase an SHA256 is generated from the contents of the
compiled files, and inserted into the filenames as they are written to disk.
These fingerprinted names are used by the Rails helpers in place of the manifest
name.
@@ -743,22 +743,24 @@ Rails.application.config.assets.precompile += %w( admin.js admin.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 `manifest-md5hash.json` 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:
+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
+fingerprints. This is used by the Rails helper methods to avoid handing the
+mapping requests back to Sprockets. A typical manifest file looks like:
```ruby
-{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506,
-"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560,
-"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591,
-"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406,
-"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646,
-"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js":
-"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
-"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
-"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
-"my_image-231a680f23887d9dd70710ea5efd3c62.png"}}
+{"files":{"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383,
+"digest":"aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b","integrity":"sha256-ruS+cfEogDeueLmX3ziDMu39JGRxtTPc7aqPn+FWRCs="},
+"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css":{"logical_path":"application.css","mtime":"2016-12-23T19:12:20-05:00","size":2994,
+"digest":"86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18","integrity":"sha256-hqKStQcHk8N+LA5fOfc7s4dkTq6tp/lub8BAoCixbBg="},
+"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico":{"logical_path":"favicon.ico","mtime":"2016-12-23T20:11:00-05:00","size":8629,
+"digest":"8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda","integrity":"sha256-jSOHuNTTLOzZP6OQDfDp/4nQGqzYT1DngMF8n2s9Dto="},
+"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png":{"logical_path":"my_image.png","mtime":"2016-12-23T20:10:54-05:00","size":23414,
+"digest":"f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493","integrity":"sha256-9AKBVv1+ygNYTV8vwEcN8eDbxzaequY4sv8DP5iOxJM="}},
+"assets":{"application.js":"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js",
+"application.css":"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css",
+"favicon.ico":"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico",
+"my_image.png":"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png"}}
```
The default location for the manifest is the root of the location specified in
@@ -850,7 +852,7 @@ config.assets.compile = true
On the first request the assets are compiled and cached as outlined in
development above, and the manifest names used in the helpers are altered to
-include the MD5 hash.
+include the SHA256 hash.
Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This
signals all caches between your server and the client browser that this content
@@ -1115,7 +1117,7 @@ config.assets.js_compressor = :uglifier
```
NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme)
-supported runtime in order to use `uglifier`. If you are using Mac OS X or
+supported runtime in order to use `uglifier`. If you are using macOS or
Windows you have a JavaScript runtime installed in your operating system.
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 03d3daecc8..5794bfa666 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -154,7 +154,7 @@ case, the column definition might look like this:
```ruby
create_table :accounts do |t|
- t.belongs_to :supplier, index: true, unique: true, foreign_key: true
+ t.belongs_to :supplier, index: { unique: true }, foreign_key: true
# ...
end
```
@@ -582,14 +582,30 @@ class CreateBooks < ActiveRecord::Migration[5.0]
t.string :book_number
t.integer :author_id
end
-
- add_index :books, :author_id
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:
+
+```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
+ end
+
+ add_index :books, :author_id
+ add_foreign_key :books, :authors
+ end
+end
+```
+
#### 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.
@@ -709,55 +725,73 @@ class Book < ApplicationRecord
end
```
-By default, Active Record doesn't know about the connection between these associations. This can lead to two copies of an object getting out of sync:
+Active Record will attempt to automatically identify that these two models share a bi-directional association based on the association name. In this way, Active Record will only load one copy of the `Author` object, making your application more efficient and preventing inconsistent data:
```ruby
a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
-a.first_name = 'Manny'
-a.first_name == b.author.first_name # => false
+a.first_name = 'David'
+a.first_name == b.author.first_name # => true
```
-This happens because `a` and `b.author` are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations:
+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:
+
+* `:conditions`
+* `:through`
+* `:polymorphic`
+* `:class_name`
+* `:foreign_key`
+
+For example, consider the following model declarations:
```ruby
class Author < ApplicationRecord
- has_many :books, inverse_of: :author
+ has_many :books
end
class Book < ApplicationRecord
- belongs_to :author, inverse_of: :books
+ belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
```
-With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient:
+Active Record will no longer automatically recognize the bi-directional association:
```ruby
a = Author.first
b = a.books.first
-a.first_name == b.author.first_name # => true
-a.first_name = 'Manny'
-a.first_name == b.author.first_name # => true
+a.first_name == b.writer.first_name # => true
+a.first_name = 'David'
+a.first_name == b.writer.first_name # => false
+```
+
+Active Record provides the `:inverse_of` option so you can explicitly declare bi-directional associations:
+
+```ruby
+class Author < ApplicationRecord
+ has_many :books, inverse_of: 'writer'
+end
+
+class Book < ApplicationRecord
+ belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
+end
```
-There are a few limitations to `inverse_of` support:
+By including the `:inverse_of` option in the `has_many` association declaration, Active Record will now recognize the bi-directional association:
+
+```ruby
+a = Author.first
+b = a.books.first
+a.first_name == b.writer.first_name # => true
+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.
-* For `belongs_to` associations, `has_many` inverse associations are ignored.
-
-Every association will attempt to automatically find the inverse association
-and set the `:inverse_of` option heuristically (based on the association name).
-Most associations with standard names will be supported. However, associations
-that contain the following options will not have their inverses set
-automatically:
-
-* `:conditions`
-* `:through`
-* `:polymorphic`
-* `:foreign_key`
Detailed Association Reference
------------------------------
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index fd7626250c..6cdce5c2f4 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -387,6 +387,11 @@ store is not appropriate for large application deployments. However, it can
work well for small, low traffic sites with only a couple of server processes,
as well as development and test environments.
+New Rails projects are configured to use this implementation in development environment by default.
+
+NOTE: Since processes will not share cache data when using `:memory_store`,
+it will not be possible to manually read, write or expire the cache via the Rails console.
+
### ActiveSupport::Cache::FileStore
This cache store uses the file system to store entries. The path to the directory where the store files will be stored must be specified when initializing the cache.
@@ -396,14 +401,15 @@ config.cache_store = :file_store, "/path/to/cache/directory"
```
With this cache store, multiple server processes on the same host can share a
-cache. The cache store is appropriate for low to medium traffic sites that are
+cache. This cache store is appropriate for low to medium traffic sites that are
served off one or two hosts. Server processes running on different hosts could
share a cache by using a shared file system, but that setup is not recommended.
As the cache will grow until the disk is full, it is recommended to
periodically clear out old entries.
-This is the default cache store implementation.
+This is the default cache store implementation (at `"#{root}/tmp/cache/"`) if
+no explicit `config.cache_store` is supplied.
### ActiveSupport::Cache::MemCacheStore
@@ -570,6 +576,20 @@ You can also set the strong ETag directly on the response.
response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"
```
+Caching in Development
+----------------------
+
+It's common to want to test the caching strategy of your application
+in development mode. Rails provides the rake task `dev:cache` to
+easily toggle caching on/off.
+
+```bash
+$ bin/rails dev:cache
+Development mode is now being cached.
+$ bin/rails dev:cache
+Development mode is no longer being cached.
+```
+
References
----------
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 9d7ecce947..3360496c08 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -63,7 +63,7 @@ With no further work, `rails server` will run our new shiny Rails app:
$ cd commandsapp
$ bin/rails server
=> Booting Puma
-=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
+=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas
@@ -294,7 +294,7 @@ If you wish to test out some code without changing any data, you can do that by
```bash
$ bin/rails console --sandbox
-Loading development environment in sandbox (Rails 5.0.0)
+Loading development environment in sandbox (Rails 5.1.0)
Any modifications you make will be rolled back on exit
irb(main):001:0>
```
@@ -407,8 +407,8 @@ db:fixtures:load Loads fixtures into the ...
db:migrate Migrate the database ...
db:migrate:status Display status of migrations
db:rollback Rolls the schema back to ...
-db:schema:cache:clear Clears a db/schema_cache.dump file
-db:schema:cache:dump Creates a db/schema_cache.dump file
+db:schema:cache:clear Clears a db/schema_cache.yml file
+db:schema:cache:dump Creates a db/schema_cache.yml file
db:schema:dump Creates a db/schema.rb file ...
db:schema:load Loads a schema.rb file ...
db:seed Loads the seed data ...
@@ -428,12 +428,12 @@ 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.0.0
+Rails version 5.1.0
Ruby version 2.2.2 (x86_64-linux)
RubyGems version 2.4.6
-Rack version 1.6
+Rack version 2.0.1
JavaScript Runtime Node.js (V8)
-Middleware Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag
+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
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index b0334bfe4a..ae70b06996 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -32,7 +32,7 @@ Configuring Rails Components
In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The configuration file `config/application.rb` and environment-specific configuration files (such as `config/environments/production.rb`) allow you to specify the various settings that you want to pass down to all of the components.
-For example, the `config/application.rb` file includes this setting:
+For example, you could add this setting to `config/application.rb` file:
```ruby
config.time_zone = 'Central Time (US & Canada)'
@@ -108,7 +108,7 @@ application. Accepts a valid week day symbol (e.g. `:monday`).
you don't want shown in the logs, such as passwords or credit card
numbers. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression.
-* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details.
+* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://api.rubyonrails.org/classes/ActionDispatch/SSL.html) for details.
* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you.
@@ -350,9 +350,9 @@ All these configuration options are delegated to the `I18n` library.
`config/environments/production.rb` which is generated by Rails. The
default value is `true` if this configuration is not set.
-* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling db:structure:dump.
- The options are `:schema_search_path` (the default) which dumps any schemas listed in schema_search_path,
- `:all` which always dumps all schemas regardless of the schema_search_path,
+* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling `db:structure:dump`.
+ The options are `:schema_search_path` (the default) which dumps any schemas listed in `schema_search_path`,
+ `:all` which always dumps all schemas regardless of the `schema_search_path`,
or a string of comma separated schemas.
* `config.active_record.belongs_to_required_by_default` is a boolean value and
@@ -362,12 +362,17 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.warn_on_records_fetched_greater_than` allows setting a
warning threshold for query result size. If the number of records returned
by a query exceeds the threshold, a warning is logged. This can be used to
- identify queries which might be causing memory bloat.
+ identify queries which might be causing a memory bloat.
* `config.active_record.index_nested_attribute_errors` allows errors for nested
- has_many relationships to be displayed with an index as well as the error.
+ `has_many` relationships to be displayed with an index as well as the error.
Defaults to `false`.
+* `config.active_record.use_schema_cache_dump` enables users to get schema cache information
+ from `db/schema_cache.yml` (generated by `bin/rails db:schema:cache:dump`), instead of
+ having to send a query to the database to get this information.
+ Defaults to `true`.
+
The MySQL adapter adds one additional configuration option:
* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`.
@@ -626,8 +631,6 @@ There are a few configuration options available in Active Support:
* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
-* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns `false` (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `new_framework_defaults.rb` which sets the value to `false`. This file is *not* added when running `rails app:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code.
-
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
* `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations.
@@ -1305,7 +1308,7 @@ end
Otherwise, in every request Rails walks the application tree to check if
anything has changed.
-On Linux and Mac OS X no additional gems are needed, but some are required
+On Linux and macOS no additional gems are needed, but some are required
[for *BSD](https://github.com/guard/listen#on-bsd) and
[for Windows](https://github.com/guard/listen#on-windows).
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 830a546570..3b19b0dff1 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -15,7 +15,7 @@ After reading this guide, you will know:
Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
-As mentioned in [Rails
+As mentioned in [Rails'
README](https://github.com/rails/rails/blob/master/README.md), everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/).
--------------------------------------------------------------------------------
@@ -67,7 +67,7 @@ can expect it to be marked "invalid" as soon as it's reviewed.
Sometimes, the line between 'bug' and 'feature' is a hard one to draw.
Generally, a feature is anything that adds new behavior, while a bug is
anything that causes incorrect behavior. Sometimes,
-the core team will have to make a judgement call. That said, the distinction
+the core team will have to make a judgment call. That said, the distinction
generally just affects which release your patch will get in to; we love feature
submissions! They just won't get backported to maintenance branches.
@@ -98,13 +98,13 @@ Anything you can do to make bug reports more succinct or easier to reproduce hel
### Testing Patches
-You can also help out by examining pull requests that have been submitted to Ruby on Rails via GitHub. To apply someone's changes you need first to create a dedicated branch:
+You can also help out by examining pull requests that have been submitted to Ruby on Rails via GitHub. In order to apply someone's changes, you need to first create a dedicated branch:
```bash
$ git checkout -b testing_branch
```
-Then you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails.
+Then, you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails.
```bash
$ git remote add JohnSmith https://github.com/JohnSmith/rails.git
@@ -335,10 +335,12 @@ file.
#### Testing Active Record
-First, create the databases you'll need. For MySQL and PostgreSQL,
-running the SQL statements `create database activerecord_unittest` and
-`create database activerecord_unittest2` is sufficient. This is not
-necessary for SQLite3.
+First, create the databases you'll need. You can find a list of the required
+table names, usernames, and passwords in `activerecord/test/config.example.yml`.
+
+For MySQL and PostgreSQL, running the SQL statements `create database
+activerecord_unittest` and `create database activerecord_unittest2` is
+sufficient. This is not necessary for SQLite3.
This is how you run the Active Record test suite only for SQLite3:
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index df3003a6a8..58aab774b3 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -313,7 +313,7 @@ For example:
```bash
=> Booting Puma
-=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
+=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.4.0 (ruby 2.3.1-p112), codename: Owl Bowl Brawl
@@ -445,11 +445,11 @@ then `backtrace` will supply the answer.
--> #0 ArticlesController.index
at /PathToProject/app/controllers/articles_controller.rb:8
#1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array)
- at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/basic_implicit_render.rb:4
+ at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/basic_implicit_render.rb:4
#2 AbstractController::Base.process_action(action#NilClass, *args#Array)
- at /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb:181
+ at /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb:181
#3 ActionController::Rendering.process_action(action, *args)
- at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:30
+ at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/rendering.rb:30
...
```
@@ -461,7 +461,7 @@ context.
```
(byebug) frame 2
-[176, 185] in /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb
+[176, 185] in /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb
176: # is the intended way to override action dispatching.
177: #
178: # Notice that the first argument is the method to be dispatched
@@ -606,7 +606,6 @@ You can also inspect for an object method this way:
@new_record = true
@readonly = false
@transaction_state = nil
-@txn = nil
```
You can also use `display` to start watching variables. This is a good way of
@@ -677,13 +676,13 @@ Ruby instruction to be executed -- in this case, Active Support's `week` method.
```
(byebug) step
-[49, 58] in /PathToGems/activesupport-5.0.0/lib/active_support/core_ext/numeric/time.rb
+[49, 58] in /PathToGems/activesupport-5.1.0/lib/active_support/core_ext/numeric/time.rb
49:
50: # Returns a Duration instance matching the number of weeks provided.
51: #
52: # 2.weeks # => 14 days
53: def weeks
-=> 54: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
+=> 54: ActiveSupport::Duration.weeks(self)
55: end
56: alias :week :weeks
57:
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index 20cd34c182..7ec038eb4d 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -46,7 +46,7 @@ $ cd rails
The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests.
-Install first SQLite3 and its development files for the `sqlite3` gem. Mac OS X
+Install first SQLite3 and its development files for the `sqlite3` gem. On macOS
users are done with:
```bash
@@ -162,6 +162,10 @@ $ cd actionpack
$ bundle exec ruby -Itest path/to/test.rb -n test_name
```
+### Railties Setup
+
+Some Railties tests depend on a JavaScript runtime environment, such as having [Node.js](https://nodejs.org/) installed.
+
### Active Record Setup
Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them.
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 2925fb4b58..5fccdcccec 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -194,6 +194,11 @@
url: upgrading_ruby_on_rails.html
description: This guide helps in upgrading applications to latest Ruby on Rails versions.
-
+ name: Ruby on Rails 5.1 Release Notes
+ url: 5_1_release_notes.html
+ description: Release notes for Rails 5.1.
+ work_in_progress: true
+ -
name: Ruby on Rails 5.0 Release Notes
url: 5_0_release_notes.html
description: Release notes for Rails 5.0.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 0020112a1c..180a786237 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -59,7 +59,7 @@ only be enhancing it, rather than changing it drastically.
To see demonstrations of other engines, check out
[Devise](https://github.com/plataformatec/devise), an engine that provides
authentication for its parent applications, or
-[Forem](https://github.com/radar/forem), an engine that provides forum
+[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.
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 048fe190e8..0508b0fb38 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -438,8 +438,6 @@ output:
Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option.
-TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings.
-
WARNING: When `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true.
You can add arbitrary attributes to the options using hashes:
@@ -533,7 +531,7 @@ To leverage time zone support in Rails, you have to ask your users what time zon
<%= time_zone_select(:person, :time_zone) %>
```
-There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the API documentation to learn about the possible arguments for these two methods.
+There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-time_zone_options_for_select) to learn about the possible arguments for these two methods.
Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/stefanpenner/country_select). When using this, be aware that the exclusion or inclusion of certain names from the list can be somewhat controversial (and was the reason this functionality was extracted from Rails).
diff --git a/guides/source/generators.md b/guides/source/generators.md
index 32bbdc554a..d0b6cef3fd 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -208,7 +208,15 @@ $ bin/rails generate scaffold User name:string
Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication.
-Our first customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following:
+If we want to avoid generating the default `app/assets/stylesheets/scaffolds.scss` file when scaffolding a new resource we can disable `scaffold_stylesheet`:
+
+```ruby
+ config.generators do |g|
+ g.scaffold_stylesheet false
+ end
+```
+
+The next customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following:
```ruby
config.generators do |g|
@@ -451,6 +459,26 @@ $ rails new thud -m https://gist.github.com/radar/722911/raw/
Whilst the final section of this guide doesn't cover how to generate the most awesome template known to man, it will take you through the methods available at your disposal so that you can develop it yourself. These same methods are also available for generators.
+Adding Command Line Arguments
+-----------------------------
+Rails generators can be easily modified to accept custom command line arguments. This functionality comes from [Thor](http://www.rubydoc.info/github/erikhuda/thor/master/Thor/Base/ClassMethods#class_option-instance_method):
+
+```
+class_option :scope, type: :string, default: 'read_products'
+```
+
+Now our generator can be invoked as follows:
+
+```bash
+rails generate initializer --scope write_products
+```
+
+The command line arguments are accessed through the `options` method inside the generator class. e.g:
+
+```ruby
+@scope = options['scope']
+```
+
Generator methods
-----------------
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index c04d42d743..068114898d 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -86,7 +86,7 @@ your prompt will look something like `c:\source_code>`
### Installing Rails
-Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose
+Open up a command line prompt. On macOS open Terminal.app, on Windows choose
"Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a
dollar sign `$` should be run in the command line. Verify that you have a
current version of Ruby installed:
@@ -98,7 +98,7 @@ ruby 2.3.1p112
TIP: A number of tools exist to help you quickly install Ruby and Ruby
on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org),
-while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp).
+while macOS users can use [Tokaido](https://github.com/tokaido/tokaidoapp).
For more installation methods for most Operating Systems take a look at
[ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/).
@@ -127,7 +127,7 @@ run the following:
$ rails --version
```
-If it says something like "Rails 5.0.0", you are ready to continue.
+If it says something like "Rails 5.1.0", you are ready to continue.
### Creating the Blog Application
@@ -206,7 +206,7 @@ folder directly to the Ruby interpreter e.g. `ruby bin\rails server`.
TIP: Compiling CoffeeScript and JavaScript asset compression requires you
have a JavaScript runtime available on your system, in the absence
of a runtime you will see an `execjs` error during asset compilation.
-Usually Mac OS X and Windows come with a JavaScript runtime installed.
+Usually macOS and Windows come with a JavaScript runtime installed.
Rails adds the `therubyracer` gem to the generated `Gemfile` in a
commented line for new apps and you can uncomment if you need it.
`therubyrhino` is the recommended runtime for JRuby users and is added by
@@ -221,7 +221,7 @@ your application in action, open a browser window and navigate to
TIP: To stop the web server, hit Ctrl+C in the terminal window where it's
running. To verify the server has stopped you should see your command prompt
-cursor again. For most UNIX-like systems including Mac OS X this will be a
+cursor again. For most UNIX-like systems including macOS this will be a
dollar sign `$`. In development mode, Rails does not generally require you to
restart the server; changes you make in files will be automatically picked up by
the server.
@@ -474,7 +474,7 @@ one here because the `ArticlesController` inherits from `ApplicationController`.
The next part of the message contains `request.formats` which specifies
the format of template to be served in response. It is set to `text/html` as we
requested this page via browser, so Rails is looking for an HTML template.
-`request.variants` specifies what kind of physical devices would be served by
+`request.variant` specifies what kind of physical devices would be served by
the response and helps Rails determine which template to use in the response.
It is empty because no information has been provided.
@@ -827,7 +827,7 @@ NOTE: A frequent practice is to place the standard CRUD actions in each
controller in the following order: `index`, `show`, `new`, `edit`, `create`, `update`
and `destroy`. You may use any order you choose, but keep in mind that these
are public methods; as mentioned earlier in this guide, they must be placed
-before any private or protected method in the controller in order to work.
+before declaring `private` visibility in the controller.
Given that, let's add the `show` action, as follows:
@@ -1157,7 +1157,7 @@ it look as follows:
```html+erb
<h1>Edit article</h1>
-<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
+<%= form_for(@article) do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
@@ -1195,14 +1195,15 @@ it look as follows:
This time we point the form to the `update` action, which is not defined yet
but will be very soon.
-The `method: :patch` option tells Rails that we want this form to be submitted
+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.
The first parameter of `form_for` can be an object, say, `@article` which would
cause the helper to fill in the form with the fields of the object. Passing in a
symbol (`:article`) with the same name as the instance variable (`@article`)
-also automagically leads to the same behavior. This is what is happening here.
+also automagically leads to the same behavior.
More details can be found in [form_for documentation]
(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for).
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index fd54bca4ff..6c8706bc13 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -72,11 +72,13 @@ I18n.l Time.now
There are also attribute readers and writers for the following attributes:
```ruby
-load_path # Announce your custom translation files
-locale # Get and set the current locale
-default_locale # Get and set the default locale
-exception_handler # Use a different exception_handler
-backend # Use a different backend
+load_path # Announce your custom translation files
+locale # Get and set the current locale
+default_locale # Get and set the default locale
+available_locales # Whitelist locales available for the application
+enforce_available_locales # Enforce locale whitelisting (true or false)
+exception_handler # Use a different exception_handler
+backend # Use a different backend
```
So, let's internationalize a simple Rails application from the ground up in the next chapters!
@@ -124,6 +126,9 @@ The load path must be specified before any translations are looked up. To change
# Where the I18n library should search for translation files
I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
+# Whitelist locales available for the application
+I18n.available_locales = [:en, :pt]
+
# Set default locale to something other than :en
I18n.default_locale = :pt
```
@@ -404,6 +409,35 @@ NOTE: You need to restart the server when you add new locale files.
You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.)
+If your translations are stored in YAML files, certain keys must be escaped. They are:
+
+* true, on, yes
+* false, off, no
+
+Examples:
+
+```erb
+# config/locales/en.yml
+en:
+ success:
+ 'true': 'True!'
+ 'on': 'On!'
+ 'false': 'False!'
+ failure:
+ true: 'True!'
+ off: 'Off!'
+ false: 'False!'
+```
+
+```ruby
+I18n.t 'success.true' # => 'True!'
+I18n.t 'success.on' # => 'On!'
+I18n.t 'success.false' # => 'False!'
+I18n.t 'failure.false' # => Translation Missing
+I18n.t 'failure.off' # => Translation Missing
+I18n.t 'failure.true' # => Translation Missing
+```
+
### Passing Variables to Translations
One key consideration for successfully internationalizing an application is to
@@ -667,12 +701,13 @@ end
### Pluralization
-In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature.
+In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar), [Japanese](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ja), [Russian](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://cldr.unicode.org/index/cldr-spec/plural-rules). Thus, the I18n API provides a flexible pluralization feature.
The `:count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR:
```ruby
I18n.backend.store_translations :en, inbox: {
+ zero: 'no messages', # optional
one: 'one message',
other: '%{count} messages'
}
@@ -681,15 +716,20 @@ I18n.translate :inbox, count: 2
I18n.translate :inbox, count: 1
# => 'one message'
+
+I18n.translate :inbox, count: 0
+# => 'no messages'
```
The algorithm for pluralizations in `:en` is as simple as:
```ruby
-entry[count == 1 ? 0 : 1]
+lookup_key = :zero if count == 0 && entry.has_key?(:zero)
+lookup_key ||= count == 1 ? :one : :other
+entry[lookup_key]
```
-I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero).
+The translation denoted as `:one` is regarded as singular, and the `:other` is used as plural. If the count is zero, and a `:zero` entry is present, then it will be used instead of `:other`.
If the lookup for the key does not return a Hash suitable for pluralization, an `I18n::InvalidPluralizationData` exception is raised.
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index 57ed35d0d8..3ea156c6fe 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -74,7 +74,7 @@ This file is as follows:
```ruby
#!/usr/bin/env ruby
-APP_PATH = File.expand_path('../../config/application', __FILE__)
+APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
```
@@ -86,7 +86,7 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot
`config/boot.rb` contains:
```ruby
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
```
@@ -131,7 +131,7 @@ Once `config/boot.rb` has finished, the next file that is required is
`ARGV` array simply contains `server` which will be passed over:
```ruby
-ARGV << '--help' if ARGV.empty?
+require "rails/command"
aliases = {
"g" => "generate",
@@ -146,33 +146,37 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
-require 'rails/commands/commands_tasks'
-
-Rails::CommandsTasks.new(ARGV).run_command!(command)
+Rails::Command.invoke command, ARGV
```
-TIP: As you can see, an empty ARGV list will make Rails show the help
-snippet.
-
If we had used `s` rather than `server`, Rails would have used the `aliases`
defined here to find the matching command.
-### `rails/commands/commands_tasks.rb`
+### `rails/command.rb`
-When one types a valid Rails command, `run_command!` a method of the same name
-is called. If Rails doesn't recognize the command, it tries to run a Rake task
-of the same name.
+When one types a Rails command, `invoke` tries to lookup a command for the given
+namespace and executing the command if found.
-```ruby
-COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help)
+If Rails doesn't recognize the command, it hands the reins over to Rake
+to run a task of the same name.
-def run_command!(command)
- command = parse_command(command)
+As shown, `Rails::Command` displays the help output automatically if the `args`
+are empty.
- if COMMAND_WHITELIST.include?(command)
- send(command)
- else
- run_rake_task(command)
+```ruby
+module Rails::Command
+ class << self
+ def invoke(namespace, args = [], **config)
+ namespace = namespace.to_s
+ namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace)
+ namespace = "version" if %w( -v --version ).include? namespace
+
+ if command = find_by_namespace(namespace)
+ command.perform(namespace, args, config)
+ else
+ find_by_namespace("rake").perform(namespace, args, config)
+ end
+ end
end
end
```
@@ -180,53 +184,39 @@ end
With the `server` command, Rails will further run the following code:
```ruby
-def set_application_directory!
- Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
-end
-
-def server
- set_application_directory!
- require_command!("server")
-
- Rails::Server.new.tap do |server|
- # We need to require application after the server sets environment,
- # otherwise the --environment option given to the server won't propagate.
- require APP_PATH
- Dir.chdir(Rails.application.root)
- server.start
+module Rails
+ module Command
+ class ServerCommand < Base # :nodoc:
+ def perform
+ set_application_directory!
+
+ Rails::Server.new.tap do |server|
+ # Require application after server sets environment to propagate
+ # the --environment option.
+ require APP_PATH
+ Dir.chdir(Rails.application.root)
+ server.start
+ end
+ end
+ end
end
end
-
-def require_command!(command)
- require "rails/commands/#{command}"
-end
```
This file will change into the Rails root directory (a path two directories up
from `APP_PATH` which points at `config/application.rb`), but only if the
-`config.ru` file isn't found. This then requires `rails/commands/server` which
-sets up the `Rails::Server` class.
-
-```ruby
-require 'fileutils'
-require 'optparse'
-require 'action_dispatch'
-require 'rails'
-
-module Rails
- class Server < ::Rack::Server
-```
-
-`fileutils` and `optparse` are standard Ruby libraries which provide helper functions for working with files and parsing options.
+`config.ru` file isn't found. This then starts up the `Rails::Server` class.
### `actionpack/lib/action_dispatch.rb`
Action Dispatch is the routing component of the Rails framework.
It adds functionality like routing, session, and common middlewares.
-### `rails/commands/server.rb`
+### `rails/commands/server/server_command.rb`
-The `Rails::Server` class is defined in this file by inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`:
+The `Rails::Server` class is defined in this file by inheriting from
+`Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize`
+method in `rails/commands/server/server_command.rb`:
```ruby
def initialize(*)
@@ -252,7 +242,10 @@ end
In this case, `options` will be `nil` so nothing happens in this method.
-After `super` has finished in `Rack::Server`, we jump back to `rails/commands/server.rb`. At this point, `set_environment` is called within the context of the `Rails::Server` object and this method doesn't appear to do much at first glance:
+After `super` has finished in `Rack::Server`, we jump back to
+`rails/commands/server/server_command.rb`. At this point, `set_environment`
+is called within the context of the `Rails::Server` object and this method
+doesn't appear to do much at first glance:
```ruby
def set_environment
@@ -289,17 +282,15 @@ With the `default_options` set to this:
```ruby
def default_options
- environment = ENV['RACK_ENV'] || 'development'
- default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
- {
- :environment => environment,
- :pid => nil,
- :Port => 9292,
- :Host => default_host,
- :AccessLog => [],
- :config => "config.ru"
- }
+ super.merge(
+ Port: ENV.fetch("PORT", 3000).to_i,
+ Host: ENV.fetch("HOST", "localhost").dup,
+ DoNotReverseLookup: true,
+ environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup,
+ daemonize: false,
+ caching: nil,
+ pid: Options::DEFAULT_PID_PATH,
+ restart_cmd: restart_command)
end
```
@@ -311,22 +302,25 @@ def opt_parser
end
```
-The class **is** defined in `Rack::Server`, but is overwritten in `Rails::Server` to take different arguments. Its `parse!` method begins like this:
+The class **is** defined in `Rack::Server`, but is overwritten in
+`Rails::Server` to take different arguments. Its `parse!` method looks
+like this:
```ruby
def parse!(args)
args, options = args.dup, {}
- opt_parser = OptionParser.new do |opts|
- opts.banner = "Usage: rails server [puma, thin, etc] [options]"
- opts.on("-p", "--port=port", Integer,
- "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
- ...
+ option_parser(options).parse! args
+
+ options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
+ options[:server] = args.shift
+ options
+end
```
This method will set up keys for the `options` which Rails will then be
able to use to determine how its server should run. After `initialize`
-has finished, we jump back into `rails/server` where `APP_PATH` (which was
+has finished, we jump back into the server command where `APP_PATH` (which was
set earlier) is required.
### `config/application`
@@ -345,6 +339,7 @@ def start
print_boot_information
trap(:INT) { exit }
create_tmp_directories
+ setup_dev_caching
log_to_stdout if options[:log_stdout]
super
@@ -352,7 +347,6 @@ def start
end
private
-
def print_boot_information
...
puts "=> Run `rails server -h` for more startup options"
@@ -364,21 +358,30 @@ private
end
end
+ def setup_dev_caching
+ if options[:environment] == "development"
+ Rails::DevCaching.enable_by_argument(options[:caching])
+ end
+ end
+
def log_to_stdout
wrapped_app # touch the app so the logger is set up
-
- console = ActiveSupport::Logger.new($stdout)
+
+ console = ActiveSupport::Logger.new(STDOUT)
console.formatter = Rails.logger.formatter
console.level = Rails.logger.level
-
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+
+ unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+ end
end
```
This is where the first output of the Rails initialization happens. This method
creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the
process. As we can see from the code here, it will create the `tmp/cache`,
-`tmp/pids`, and `tmp/sockets` directories. It then calls `wrapped_app` which is
+`tmp/pids`, and `tmp/sockets` directories. It then enables caching in development
+if `rails server` is called with `--dev-caching`. Finally, it calls `wrapped_app` which is
responsible for creating the Rack app, before creating and assigning an instance
of `ActiveSupport::Logger`.
@@ -538,7 +541,7 @@ require "rails"
sprockets/railtie
).each do |railtie|
begin
- require "#{railtie}"
+ require railtie
rescue LoadError
end
end
diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb
index 547abcbc19..63eeb007d7 100644
--- a/guides/source/kindle/rails_guides.opf.erb
+++ b/guides/source/kindle/rails_guides.opf.erb
@@ -5,7 +5,7 @@
<meta name="cover" content="cover" />
<dc-metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
- <dc:title>Ruby on Rails Guides (<%= @version %>)</dc:title>
+ <dc:title>Ruby on Rails Guides (<%= @version || "master@#{@edge[0, 7]}" %>)</dc:title>
<dc:language>en-us</dc:language>
<dc:creator>Ruby on Rails</dc:creator>
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index c8702f54fc..48bb3147f3 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -411,6 +411,8 @@ render formats: :xml
render formats: [:json, :xml]
```
+If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised.
+
#### Finding Layouts
To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions.
@@ -1155,7 +1157,7 @@ To pass a local variable to a partial in only specific cases use the `local_assi
<%= render article, full: true %>
```
-* `_articles.html.erb`
+* `_article.html.erb`
```erb
<h2><%= article.title %></h2>
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
index 7ced3eab1c..1d6a4edb5b 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.0.Z`, `4.2.Z`.
+**Currently included series:** `5.1.Z`.
Security Issues
---------------
@@ -59,7 +59,7 @@ 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.0.Z`, `4.2.Z`.
+**Currently included series:** `5.1.Z`, `5.0.Z`.
Severe Security Issues
----------------------
@@ -68,7 +68,7 @@ For severe security issues we will provide new versions as above, 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.0.Z`, `4.2.Z`.
+**Currently included series:** `5.1.Z`, `5.0.Z`, `4.2.Z`.
Unsupported Release Series
--------------------------
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 937e313663..86492a9332 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -603,6 +603,14 @@ get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`.
+You can also use `defaults` in a block format to define the defaults for multiple items:
+
+```ruby
+defaults format: :json do
+ resources :photos
+end
+```
+
NOTE: You cannot override defaults via query parameters - this is for security reasons. The only defaults that can be overridden are dynamic segments via substitution in the URL path.
### Naming Routes
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index 50866350f8..de63e193f4 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -50,6 +50,48 @@ Use the same inline formatting as regular text:
##### The `:content_type` Option
```
+Linking to the API
+------------------
+
+Links to the API (`api.rubyonrails.org`) are processed by the guides generator in the following manner:
+
+Links that include a release tag are left untouched. For example
+
+```
+http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html
+```
+
+is not modified.
+
+Please use these in release notes, since they should point to the corresponding version no matter the target being generated.
+
+If the link does not include a release tag and edge guides are being generated, the domain is replaced by `edgeapi.rubyonrails.org`. For example,
+
+```
+http://api.rubyonrails.org/classes/ActionDispatch/Response.html
+```
+
+becomes
+
+```
+http://edgeapi.rubyonrails.org/classes/ActionDispatch/Response.html
+```
+
+If the link does not include a release tag and release guides are being generated, the Rails version is injected. For example, if we are generating the guides for v5.1.0 the link
+
+```
+http://api.rubyonrails.org/classes/ActionDispatch/Response.html
+```
+
+becomes
+
+```
+http://api.rubyonrails.org/v5.1.0/classes/ActionDispatch/Response.html
+```
+
+Please don't link to `edgeapi.rubyonrails.org` manually.
+
+
API Documentation Guidelines
----------------------------
@@ -97,8 +139,6 @@ By default, guides that have not been modified are not processed, so `ONLY` is r
To force processing all the guides, pass `ALL=1`.
-It is also recommended that you work with `WARNINGS=1`. This detects duplicate IDs and warns about broken internal links.
-
If you want to generate guides in a language other than English, you can keep them in a separate directory under `source` (eg. `source/es`) and use the `GUIDES_LANGUAGE` environment variable:
```
diff --git a/guides/source/security.md b/guides/source/security.md
index bb67eb75d9..7e27e6f37d 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -212,7 +212,7 @@ CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less th
NOTE: _First, as is required by the W3C, use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF._
-The HTTP protocol basically provides two main types of requests - GET and POST (and more, but they are not supported by most browsers). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST:
+The HTTP protocol basically provides two main types of requests - GET and POST (DELETE, PUT, and PATCH should be used like POST). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST:
**Use GET if:**
@@ -224,7 +224,7 @@ The HTTP protocol basically provides two main types of requests - GET and POST (
* The interaction _changes the state_ of the resource in a way that the user would perceive (e.g., a subscription to a service), or
* The user is _held accountable for the results_ of the interaction.
-If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier.
+If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Some legacy web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle these cases.
_POST requests can be sent automatically, too_. In this example, the link www.harmless.com is shown as the destination in the browser's status bar. But it has actually dynamically created a new form that sends a POST request.
@@ -257,13 +257,12 @@ protect_from_forgery with: :exception
This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
-NOTE: By default, Rails includes jQuery and an [unobtrusive scripting adapter for
-jQuery](https://github.com/rails/jquery-ujs), which adds a header called
-`X-CSRF-Token` on every non-GET Ajax call made by jQuery with the security token.
-Without this header, non-GET Ajax requests won't be accepted by Rails. When using
-another library to make Ajax calls, it is necessary to add the security token as
-a default header for Ajax calls in your library. To get the token, have a look at
-`<meta name='csrf-token' content='THE-TOKEN'>` tag printed by
+NOTE: By default, Rails includes an [unobtrusive scripting adapter](https://github.com/rails/rails-ujs),
+which adds a header called `X-CSRF-Token` with the security token on every non-GET
+Ajax call. Without this header, non-GET Ajax requests won't be accepted by Rails.
+When using another library to make Ajax calls, it is necessary to add the security
+token as a default header for Ajax calls in your library. To get the token, have
+a look at `<meta name='csrf-token' content='THE-TOKEN'>` tag printed by
`<%= csrf_meta_tags %>` in your application view.
It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself:
@@ -377,7 +376,7 @@ In 2007 there was the first tailor-made trojan which stole information from an I
Having one single place in the admin interface or Intranet, where the input has not been sanitized, makes the entire application vulnerable. Possible exploits include stealing the privileged administrator's cookie, injecting an iframe to steal the administrator's password or installing malicious software through browser security holes to take over the administrator's computer.
-Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface.
+Refer to the Injection section for countermeasures against XSS.
**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
@@ -615,7 +614,7 @@ The two dashes start a comment ignoring everything after it. So the query return
Usually a web application includes access control. The user enters their login credentials and the web application tries to find the matching record in the users table. The application grants access when it finds a record. However, an attacker may possibly bypass this check with SQL injection. The following shows a typical database query in Rails to find the first record in the users table which matches the login credentials parameters supplied by the user.
```ruby
-User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")
+User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")
```
If an attacker enters ' OR '1'='1 as the name, and ' OR '2'>'1 as the password, the resulting SQL query will be:
@@ -762,7 +761,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title))
This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags.
-As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, and &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt;`, and `&gt;`).
+As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, and &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt;`, and `&gt;`).
##### Obfuscation and Encoding Injection
diff --git a/guides/source/testing.md b/guides/source/testing.md
index bc1f78fb2a..7741834153 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -8,7 +8,7 @@ This guide covers built-in mechanisms in Rails for testing your application.
After reading this guide, you will know:
* Rails testing terminology.
-* How to write unit, functional, and integration tests for your application.
+* How to write unit, functional, integration, and system tests for your application.
* Other popular testing approaches and plugins.
--------------------------------------------------------------------------------
@@ -33,18 +33,27 @@ Rails creates a `test` directory for you as soon as you create a Rails project u
```bash
$ ls -F test
-controllers/ helpers/ mailers/ test_helper.rb
-fixtures/ integration/ models/
+controllers/ helpers/ mailers/ system/ test_helper.rb
+fixtures/ integration/ models/ application_system_test_case.rb
```
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.
+The system test directory holds system tests, which are used for full browser
+testing of your application. System tests allow you to test your application
+the way your users experience it and help you test your JavaScript as well.
+System tests inherit from Capybara and perform in browser tests for your
+application.
+
Fixtures are a way of organizing test data; they reside in the `fixtures` directory.
A `jobs` directory will also be created when an associated test is first generated.
The `test_helper.rb` file holds the default configuration for your tests.
+The `application_system_test_case.rb` holds the default configuration for your system
+tests.
+
### The Test Environment
@@ -114,7 +123,7 @@ def test_the_truth
end
```
-However only the `test` macro allows a more readable test name. You can still use regular method definitions though.
+Although you can still use regular method definitions, using the `test` macro allows for a more readable test name.
NOTE: The method name is generated by replacing spaces with underscores. The result does not need to be a valid Ruby identifier though, the name may contain punctuation characters etc. That's because in Ruby technically any string may be a method name. This may require use of `define_method` and `send` calls to function properly, but formally there's little restriction on the name.
@@ -322,7 +331,6 @@ specify to make your test failure messages clearer.
| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.|
| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`|
| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`|
-| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true, e.g. assert_send [@user, :full_name, 'Sam Smith']. This one is weird eh?|
| `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.|
The above are a subset of assertions that minitest supports. For an exhaustive &
@@ -359,6 +367,7 @@ All the basic assertions such as `assert_equal` defined in `Minitest::Assertions
* [`ActionView::TestCase`](http://api.rubyonrails.org/classes/ActionView/TestCase.html)
* [`ActionDispatch::IntegrationTest`](http://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html)
* [`ActiveJob::TestCase`](http://api.rubyonrails.org/classes/ActiveJob/TestCase.html)
+* [`ActionDispatch::SystemTestCase`](http://api.rubyonrails.org/classes/ActionDispatch/SystemTestCase.html)
Each of these classes include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests.
@@ -588,6 +597,182 @@ create test/fixtures/articles.yml
Model tests don't have their own superclass like `ActionMailer::TestCase` instead they inherit from [`ActiveSupport::TestCase`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html).
+System Testing
+--------------
+
+System tests are full-browser tests that can be used to test your application's
+JavaScript and user experience. System tests use Capybara as a base.
+
+System tests allow for running tests in either a real browser or a headless
+driver for testing full user interactions with your application.
+
+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.
+
+```bash
+$ bin/rails generate system_test users_create
+ invoke test_unit
+ create test/system/users_creates_test.rb
+```
+
+Here's what a freshly-generated system test looks like:
+
+```ruby
+require "application_system_test_case"
+
+class UsersCreatesTest < ApplicationSystemTestCase
+ # test "visiting the index" do
+ # visit users_creates_url
+ #
+ # assert_selector "h1", text: "UsersCreate"
+ # end
+end
+```
+
+By default, system tests are run with the Selenium driver, using the Chrome
+browser, and a screen size of 1400x1400. The next section explains how to
+change the default settings.
+
+### Changing the default settings
+
+Rails makes changing the default settings for system tests very simple. All
+the setup is abstracted away so you can focus on writing your tests.
+
+When you generate a new application or scaffold, an `application_system_test_case.rb` file
+is created in the test directory. This is where all the configuration for your
+system tests should live.
+
+If you want to change the default settings you can simply 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
+`application_system_test_case.rb` file do the following:
+
+```ruby
+require "test_helper"
+require "capybara/poltergeist"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :poltergeist
+end
+```
+
+The driver name is a required argument for `driven_by`. The optional arguments
+that can be passed to `driven_by` are `:using` for the browser (this will only
+be used for non-headless drivers like Selenium), and `:screen_size` to change
+the size of the screen for screenshots.
+
+```ruby
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :firefox
+end
+```
+
+If your Capybara configuration requires more setup than provided by Rails, all
+of that configuration can be put into the `application_system_test_case.rb` file.
+
+Please see [Capybara's documentation](https://github.com/teamcapybara/capybara#setup)
+for additional settings.
+
+### Screenshot Helper
+
+The `ScreenshotHelper` is a helper designed to capture screenshots of your tests.
+This can be helpful for viewing the browser at the point a test failed, or
+to view screenshots later for debugging.
+
+Two methods are provided: `take_screenshot` and `take_failed_screenshot`.
+`take_failed_screenshot` is automatically included in `after_teardown` inside
+Rails.
+
+The `take_screenshot` helper method can be included anywhere in your tests to
+take a screenshot of the browser.
+
+### Implementing a system test
+
+Now we're going to add a system test to our blog application. We'll demonstrate
+writing a system test by visiting the index page and creating a new blog article.
+
+If you used the scaffold generator, a system test skeleton is automatically
+created for you. If you did not use the generator start by creating a system
+test skeleton.
+
+```bash
+$ bin/rails generate system_test articles
+```
+
+It should have created a test file placeholder for us. With the output of the
+previous command you should see:
+
+```bash
+ invoke test_unit
+ create test/system/articles_test.rb
+```
+
+Now let's open that file and write our first assertion:
+
+```ruby
+require "application_system_test_case"
+
+class ArticlesTest < ApplicationSystemTestCase
+ test "viewing the index" do
+ visit articles_path
+ assert_selector "h1", text: "Articles"
+ end
+end
+```
+
+The test should see that there is an `h1` on the articles index page and pass.
+
+Run the system tests.
+
+```bash
+bin/rails test:system
+```
+
+NOTE: By default, running `bin/rails test` won't run your system tests.
+Make sure to run `bin/rails test:system` to actually run them.
+
+#### Creating articles system test
+
+Now let's test the flow for creating a new article in our blog.
+
+```ruby
+test "creating an article" do
+ visit articles_path
+
+ click_on "New Article"
+
+ fill_in "Title", with: "Creating an Article"
+ fill_in "Body", with: "Created this article successfully!"
+
+ click_on "Create Article"
+
+ assert_text "Creating an Article"
+end
+```
+
+The first step is to call `visit articles_path`. This will take the test to the
+articles index page.
+
+Then the `click_on "New Article"` will find the "New Article" button on the
+index page. This will redirect the browser to `/articles/new`.
+
+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
+that the text from the new article's title is on the articles index page.
+
+#### Taking it further
+
+The beauty of system testing is that it is similar to integration testing in
+that it tests the user's interaction with your controller, model, and view, but
+system testing is much more robust and actually tests your application as if
+a real user were using it. Going forward, you can test anything that the user
+themselves would do in your application such as commenting, deleting articles,
+publishing draft articles, etc.
Integration Testing
-------------------
@@ -800,6 +985,13 @@ end
Now you can try running all the tests and they should pass.
+NOTE: If you followed the steps in the Basic Authentication section, you'll need to add the following to the `setup` block to get all the tests passing:
+
+```ruby
+request.headers['Authorization'] = ActionController::HttpAuthentication::Basic.
+ encode_credentials('dhh', 'secret')
+```
+
### Available Request Types for Functional Tests
If you're familiar with the HTTP protocol, you'll know that `get` is a type of request. There are 6 request types supported in Rails functional tests:
@@ -862,7 +1054,7 @@ class ArticlesControllerTest < ActionDispatch::IntegrationTest
assert_equal "index", @controller.action_name
assert_equal "application/x-www-form-urlencoded", @request.media_type
- assert_match "Articles", @response.body
+ assert_match "Articles", @response.body
end
end
```
@@ -1248,6 +1440,10 @@ variable. We then ensure that it was sent (the first assert), then, in the
second batch of assertions, we ensure that the email does indeed contain what we
expect. The helper `read_fixture` is used to read in the content from this file.
+NOTE: `email.body.to_s` is present when there's only one (HTML or text) part present.
+If the mailer provides both, you can test your fixture against specific parts
+with `email.text_part.body.to_s` or `email.html_part.body.to_s`.
+
Here's the content of the `invite` fixture:
```
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index dda2b12a3a..3afc0e5309 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -65,6 +65,25 @@ 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.0 to Rails 5.1
+-------------------------------------
+
+For more information on changes made to Rails 5.1 please see the [release notes](5_1_release_notes.html).
+
+### Top-level `HashWithIndifferentAccess` is soft-deprecated
+
+If your application uses the the top-level `HashWithIndifferentAccess` class, you
+should slowly move your code to use the `ActiveSupport::HashWithIndifferentAccess`
+one.
+
+It is only soft-deprecated, which means that your code will not break at the
+moment and no deprecation warning will be displayed but this constant will be
+removed in the future.
+
+Also, if you have pretty old YAML documents containing dumps of such objects,
+you may need to load and dump them again to make sure that they reference
+the right constant and that loading them won't break in the future.
+
Upgrading from Rails 4.2 to Rails 5.0
-------------------------------------
@@ -140,6 +159,8 @@ See [#19034](https://github.com/rails/rails/pull/19034) for more details.
### Rails Controller Testing
+#### Extraction of some helper methods to `rails-controller-testing`
+
`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.
@@ -147,6 +168,14 @@ your Gemfile.
If you are using Rspec for testing, please see the extra configuration required in the gem's
documentation.
+#### New behavior when uploading files
+
+If you are using `ActionDispatch::Http::UploadedFile` in your tests to
+upload files, you will need to change to use the similar `Rack::Test::UploadedFile`
+class instead.
+
+See [#26404](https://github.com/rails/rails/issues/26404) for more details.
+
### Autoloading is Disabled After Booting in the Production Environment
Autoloading is now disabled after booting in the production environment by
@@ -703,7 +732,7 @@ There are a few major changes related to JSON handling in Rails 4.1.
MultiJSON has reached its [end-of-life](https://github.com/rails/rails/pull/10576)
and has been removed from Rails.
-If your application currently depend on MultiJSON directly, you have a few options:
+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
@@ -1278,6 +1307,10 @@ Also check your environment settings for `config.action_dispatch.best_standards_
Rails 4.0 removes the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`.
+#### Cache
+
+The caching method changed between Rails 3.x and 4.0. You should [change the cache namespace](http://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-store) and roll out with a cold cache.
+
### Helpers Loading Order
The order in which helpers from more than one directory are loaded has changed in Rails 4.0. Previously, they were gathered and then sorted alphabetically. After upgrading to Rails 4.0, helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use the `helpers_path` parameter, this change will only impact the way of loading helpers from engines. If you rely on the ordering, you should check if correct methods are available after upgrade. If you would like to change the order in which engines are loaded, you can use `config.railties_order=` method.
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index c1dfcab6f3..e04b3e3581 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -149,7 +149,7 @@ Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
parts: the JavaScript half and the Ruby half.
Unless you have disabled the Asset Pipeline,
-[rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js)
+[rails-ujs](https://github.com/rails/rails-ujs/blob/master/src/rails-ujs.coffee)
provides the JavaScript half, and the regular Ruby view helpers add appropriate
tags to your DOM.
diff --git a/guides/w3c_validator.rb b/guides/w3c_validator.rb
index c0a32c6b91..4671e040ca 100644
--- a/guides/w3c_validator.rb
+++ b/guides/w3c_validator.rb
@@ -32,7 +32,8 @@ include W3CValidators
module RailsGuides
class Validator
def validate
- validator = MarkupValidator.new
+ # https://github.com/w3c-validators/w3c_validators/issues/25
+ validator = NuValidator.new
STDOUT.sync = true
errors_on_guides = {}
@@ -44,11 +45,11 @@ module RailsGuides
next
end
- if results.validity
- print "."
- else
+ if results.errors.length > 0
print "E"
errors_on_guides[f] = results.errors
+ else
+ print "."
end
end
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 7db7e2e34d..6032d2e1a1 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,71 +1 @@
-* Add `:skip_sprockets` to `Rails::PluginBuilder::PASSTHROUGH_OPTIONS`
-
- *Tsukuru Tanimichi*
-
-* Allow the use of listen's 3.1.x branch
-
- *Esteban Santana Santana*
-
-* Run `Minitest.after_run` hooks when running `rails test`.
-
- *Michael Grosser*
-
-* Run `before_configuration` callbacks as soon as application constant
- inherits from `Rails::Application`.
-
- Fixes #19880.
-
- *Yuji Yaginuma*
-
-* A generated app should not include Uglifier with `--skip-javascript` option.
-
- *Ben Pickles*
-
-* Set session store to cookie store internally and remove the initializer from
- the generated app.
-
- *Prathamesh Sonpatki*
-
-* Set the server host using the `HOST` environment variable.
-
- *mahnunchik*
-
-* Add public API to register new folders for `rake notes`:
-
- config.annotations.register_directories('spec', 'features')
-
- *John Meehan*
-
-* Display name of the class defining the initializer along with the initializer
- name in the output of `rails initializers`.
-
- Before:
- disable_dependency_loading
-
- After:
- DemoApp::Application.disable_dependency_loading
-
- *ta1kt0me*
-
-* Do not run `bundle install` when generating a new plugin.
-
- Since bundler 1.12.0, the gemspec is validated so the `bundle install`
- command will fail just after the gem is created causing confusion to the
- users. This change was a bug fix to correctly validate gemspecs.
-
- *Rafael Mendonça França*
-
-* Default `config.assets.quiet = true` in the development environment. Suppress
- logging of assets requests by default.
-
- *Kevin McPhillips*
-
-* Ensure `/rails/info` routes match in development for apps with a catch-all globbing route.
-
- *Nicholas Firth-McCoy*
-
-* Added a shared section to `config/secrets.yml` that will be loaded for all environments.
-
- *DHH*
-
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/railties/CHANGELOG.md) for previous changes.
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
index 1f496cf280..f9e4444f07 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2017 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 ef9bbf3d7e..654c7bae57 100644
--- a/railties/RDOC_MAIN.rdoc
+++ b/railties/RDOC_MAIN.rdoc
@@ -57,7 +57,7 @@ can read more about Action Pack in its {README}[link:files/actionpack/README_rdo
* The \README file created within your application.
* {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html].
-* {Ruby on \Rails Tutorial}[http://www.railstutorial.org/book].
+* {Ruby on \Rails Tutorial}[https://www.railstutorial.org/book].
* {Ruby on \Rails Guides}[http://guides.rubyonrails.org].
* {The API Documentation}[http://api.rubyonrails.org].
diff --git a/railties/Rakefile b/railties/Rakefile
index 202644fb26..680ed03f75 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -18,6 +18,7 @@ namespace :test do
"lib",
"#{File.dirname(__FILE__)}/../activesupport/lib",
"#{File.dirname(__FILE__)}/../actionpack/lib",
+ "#{File.dirname(__FILE__)}/../actionview/lib",
"#{File.dirname(__FILE__)}/../activemodel/lib"
]
ruby "-w", "-I#{dash_i.join ':'}", file
diff --git a/railties/bin/test b/railties/bin/test
new file mode 100755
index 0000000000..a7beb14b27
--- /dev/null
+++ b/railties/bin/test
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require File.expand_path("../tools/test", COMPONENT_ROOT)
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index ee48043a50..00add5829d 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -7,6 +7,7 @@ require "active_support/dependencies/autoload"
require "active_support/core_ext/kernel/reporting"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/object/blank"
require "rails/application"
require "rails/version"
@@ -67,7 +68,7 @@ module Rails
# Rails.env.development? # => true
# Rails.env.production? # => false
def env
- @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
+ @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development")
end
# Sets the Rails environment.
diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb
new file mode 100644
index 0000000000..dcc491783c
--- /dev/null
+++ b/railties/lib/rails/api/generator.rb
@@ -0,0 +1,28 @@
+require "sdoc"
+
+class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc:
+ RDoc::RDoc.add_generator self
+
+ def generate_class_tree_level(classes, visited = {})
+ # Only process core extensions on the first visit.
+ if visited.empty?
+ core_exts, classes = classes.partition { |klass| core_extension?(klass) }
+
+ super.unshift([ "Core extensions", "", "", build_core_ext_subtree(core_exts, visited) ])
+ else
+ super
+ end
+ end
+
+ private
+ def build_core_ext_subtree(classes, visited)
+ classes.map do |klass|
+ [ klass.name, klass.document_self_or_methods ? klass.path : "", "",
+ generate_class_tree_level(klass.classes_and_modules, visited) ]
+ end
+ end
+
+ def core_extension?(klass)
+ klass.name != "ActiveSupport" && klass.in_files.any? { |file| file.absolute_name.include?("core_ext") }
+ end
+end
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
index bc670b1d75..49267c2329 100644
--- a/railties/lib/rails/api/task.rb
+++ b/railties/lib/rails/api/task.rb
@@ -1,4 +1,5 @@
require "rdoc/task"
+require_relative "generator"
module Rails
module API
@@ -8,8 +9,7 @@ module Rails
include: %w(
README.rdoc
lib/active_support/**/*.rb
- ),
- exclude: "lib/active_support/vendor/*"
+ )
},
"activerecord" => {
@@ -69,7 +69,11 @@ module Rails
README.rdoc
lib/**/*.rb
),
- exclude: "lib/rails/generators/rails/**/templates/**/*.rb"
+ exclude: %w(
+ lib/rails/generators/**/templates/**/*.rb
+ lib/rails/test_unit/*
+ lib/rails/api/generator.rb
+ )
}
}
@@ -80,7 +84,7 @@ module Rails
# Be lazy computing stuff to have as light impact as possible to
# the rest of tasks.
before_running_rdoc do
- load_and_configure_sdoc
+ configure_sdoc
configure_rdoc_files
setup_horo_variables
end
@@ -91,20 +95,15 @@ module Rails
# no-op
end
- def load_and_configure_sdoc
- require "sdoc"
-
+ def configure_sdoc
self.title = "Ruby on Rails API"
self.rdoc_dir = api_dir
options << "-m" << api_main
options << "-e" << "UTF-8"
- options << "-f" << "sdoc"
+ options << "-f" << "api"
options << "-T" << "rails"
- rescue LoadError
- $stderr.puts %(Unable to load SDoc, please add\n\n gem 'sdoc', require: false\n\nto the Gemfile.)
- exit 1
end
def configure_rdoc_files
@@ -147,7 +146,7 @@ module Rails
end
class RepoTask < Task
- def load_and_configure_sdoc
+ def configure_sdoc
super
options << "-g" # link to GitHub, SDoc flag
end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 3b94ae4f82..89f7b5991f 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -4,6 +4,7 @@ require "active_support/core_ext/object/blank"
require "active_support/key_generator"
require "active_support/message_verifier"
require "rails/engine"
+require "rails/secrets"
module Rails
# An Engine with the responsibility of coordinating the whole boot process.
@@ -72,7 +73,7 @@ module Rails
# on one of the applications to create a copy of the application which shares
# the configuration.
#
- # If you decide to define rake tasks, runners, or initializers in an
+ # If you decide to define Rake tasks, runners, or initializers in an
# application other than +Rails.application+, then you must run them manually.
class Application < Engine
autoload :Bootstrap, "rails/application/bootstrap"
@@ -265,8 +266,8 @@ module Rails
end
end
- # If you try to define a set of rake tasks on the instance, these will get
- # passed up to the rake tasks defined on the application's class.
+ # If you try to define a set of Rake tasks on the instance, these will get
+ # passed up to the Rake tasks defined on the application's class.
def rake_tasks(&block)
self.class.rake_tasks(&block)
end
@@ -385,18 +386,7 @@ module Rails
def secrets
@secrets ||= begin
secrets = ActiveSupport::OrderedOptions.new
- yaml = config.paths["config/secrets"].first
-
- if File.exist?(yaml)
- require "erb"
-
- all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {}
- shared_secrets = all_secrets["shared"]
- env_secrets = all_secrets[Rails.env]
-
- secrets.merge!(shared_secrets.symbolize_keys) if shared_secrets
- secrets.merge!(env_secrets.symbolize_keys) if env_secrets
- end
+ secrets.merge! Rails::Secrets.parse(config.paths["config/secrets"].existent, env: Rails.env)
# Fallback to config.secret_key_base if secrets.secret_key_base isn't set
secrets.secret_key_base ||= config.secret_key_base
@@ -511,7 +501,7 @@ module Rails
def validate_secret_key_config! #:nodoc:
if secrets.secret_key_base.blank?
- ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " +
+ ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " \
"Read the upgrade documentation to learn more about this new config option."
if secrets.secret_token.blank?
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 11da271501..4223c38146 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -2,6 +2,7 @@ require "fileutils"
require "active_support/notifications"
require "active_support/dependencies"
require "active_support/descendants_tracker"
+require "rails/secrets"
module Rails
class Application
@@ -48,8 +49,8 @@ INFO
logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR))
logger.level = ActiveSupport::Logger::WARN
logger.warn(
- "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " +
- "(ie, make it writable for user and group: chmod 0664 #{path}). " +
+ "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " \
+ "(ie, make it writable for user and group: chmod 0664 #{path}). " \
"The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
)
logger
@@ -77,6 +78,11 @@ INFO
initializer :bootstrap_hook, group: :all do |app|
ActiveSupport.run_load_hooks(:before_initialize, app)
end
+
+ initializer :set_secrets_root, group: :all do
+ Rails::Secrets.root = root
+ Rails::Secrets.read_encrypted_secrets = config.read_encrypted_secrets
+ end
end
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 810750ed35..7c49fabba5 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -3,9 +3,6 @@ require "active_support/file_update_checker"
require "rails/engine/configuration"
require "rails/source_annotation_extractor"
-require "active_support/deprecation"
-require "active_support/core_ext/string/strip" # for strip_heredoc
-
module Rails
class Application
class Configuration < ::Rails::Engine::Configuration
@@ -16,14 +13,14 @@ module Rails
:railties_order, :relative_url_root, :secret_key_base, :secret_token,
:ssl_options, :public_file_server,
:session_options, :time_zone, :reload_classes_only_on_change,
- :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading
+ :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
+ :read_encrypted_secrets, :log_level
- attr_writer :log_level
- attr_reader :encoding, :api_only, :static_cache_control
+ attr_reader :encoding, :api_only
def initialize(*)
super
- self.encoding = "utf-8"
+ self.encoding = Encoding::UTF_8
@allow_concurrency = nil
@consider_all_requests_local = false
@filter_parameters = []
@@ -37,7 +34,7 @@ module Rails
@session_store = nil
@time_zone = "UTC"
@beginning_of_week = :monday
- @log_level = nil
+ @log_level = :debug
@generators = app_generators
@cache_store = [ :file_store, "#{root}/tmp/cache/" ]
@railties_order = [:all]
@@ -54,35 +51,38 @@ module Rails
@debug_exception_response_format = nil
@x = Custom.new
@enable_dependency_loading = false
+ @read_encrypted_secrets = false
end
- def static_cache_control=(value)
- ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `config.static_cache_control` is deprecated and will be removed in Rails 5.1.
- Please use
- `config.public_file_server.headers = { 'Cache-Control' => '#{value}' }`
- instead.
- eow
+ def load_defaults(target_version)
+ case target_version.to_s
+ when "5.0"
+ if respond_to?(:action_controller)
+ action_controller.per_form_csrf_tokens = true
+ action_controller.forgery_protection_origin_check = true
+ end
- @static_cache_control = value
- end
+ ActiveSupport.to_time_preserves_timezone = true
- def serve_static_files
- ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `config.serve_static_files` is deprecated and will be removed in Rails 5.1.
- Please use `config.public_file_server.enabled` instead.
- eow
+ if respond_to?(:active_record)
+ active_record.belongs_to_required_by_default = true
+ end
- @public_file_server.enabled
- end
+ self.ssl_options = { hsts: { subdomains: true } }
+
+ when "5.1"
+ load_defaults "5.0"
- def serve_static_files=(value)
- ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `config.serve_static_files` is deprecated and will be removed in Rails 5.1.
- Please use `config.public_file_server.enabled = #{value}` instead.
- eow
+ if respond_to?(:assets)
+ assets.unknown_asset_fallback = false
+ end
+
+ when "5.2"
+ load_defaults "5.1"
- @public_file_server.enabled = value
+ else
+ raise "Unknown version #{target_version.to_s.inspect}"
+ end
end
def encoding=(value)
@@ -112,7 +112,7 @@ module Rails
@paths ||= begin
paths = super
paths.add "config/database", with: "config/database.yml"
- paths.add "config/secrets", with: "config/secrets.yml"
+ paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}"
paths.add "config/environment", with: "config/environment.rb"
paths.add "lib/templates"
paths.add "log", with: "log/#{Rails.env}.log"
@@ -151,17 +151,13 @@ module Rails
raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace
end
- def log_level
- @log_level ||= (Rails.env.production? ? :info : :debug)
- end
-
def colorize_logging
ActiveSupport::LogSubscriber.colorize_logging
end
def colorize_logging=(val)
ActiveSupport::LogSubscriber.colorize_logging = val
- self.generators.colorize_logging = val
+ generators.colorize_logging = val
end
def session_store(new_session_store = nil, **options)
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 14c0a8cbe4..8fe48feefb 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -19,7 +19,6 @@ module Rails
if config.public_file_server.enabled
headers = config.public_file_server.headers || {}
- headers["Cache-Control".freeze] = config.static_cache_control if config.static_cache_control
middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers
end
@@ -41,12 +40,11 @@ module Rails
middleware.use ::Rack::Runtime
middleware.use ::Rack::MethodOverride unless config.api_only
middleware.use ::ActionDispatch::RequestId
+ middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
- # Must come after Rack::MethodOverride to properly log overridden methods
middleware.use ::Rails::Rack::Logger, config.log_tags
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format
- middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
unless config.cache_classes
middleware.use ::ActionDispatch::Reloader, app.reloader
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index a855e8fab0..c027d06663 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -124,6 +124,7 @@ module Rails
# the hook are taken into account.
initializer :set_routes_reloader_hook do |app|
reloader = routes_reloader
+ reloader.eager_load = app.config.eager_load
reloader.execute_if_updated
reloaders << reloader
app.reloader.to_run do
diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb
index cf0a4e128f..e02ef629f2 100644
--- a/railties/lib/rails/application/routes_reloader.rb
+++ b/railties/lib/rails/application/routes_reloader.rb
@@ -4,11 +4,13 @@ module Rails
class Application
class RoutesReloader
attr_reader :route_sets, :paths
- delegate :execute_if_updated, :execute, :updated?, to: :updater
+ attr_accessor :eager_load
+ delegate :updated?, to: :updater
def initialize
@paths = []
@route_sets = []
+ @eager_load = false
end
def reload!
@@ -19,6 +21,19 @@ module Rails
revert
end
+ def execute
+ ret = updater.execute
+ route_sets.each(&:eager_load!) if eager_load
+ ret
+ end
+
+ def execute_if_updated
+ if updated = updater.execute_if_updated
+ route_sets.each(&:eager_load!) if eager_load
+ end
+ updated
+ end
+
private
def updater
diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb
index f8394b8e0a..a98e51fd28 100644
--- a/railties/lib/rails/application_controller.rb
+++ b/railties/lib/rails/application_controller.rb
@@ -2,7 +2,7 @@ class Rails::ApplicationController < ActionController::Base # :nodoc:
self.view_paths = File.expand_path("../templates", __FILE__)
layout "application"
- protected
+ private
def require_local!
unless local_request?
diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb
index 6065e78fd1..0d4e6dc5a1 100644
--- a/railties/lib/rails/command.rb
+++ b/railties/lib/rails/command.rb
@@ -15,6 +15,8 @@ module Rails
include Behavior
+ HELP_MAPPINGS = %w(-h -? --help)
+
class << self
def hidden_commands # :nodoc:
@hidden_commands ||= []
@@ -25,19 +27,27 @@ module Rails
end
# Receives a namespace, arguments and the behavior to invoke the command.
- def invoke(namespace, args = [], **config)
- namespace = namespace.to_s
- namespace = "help" if namespace.blank? || Thor::HELP_MAPPINGS.include?(namespace)
- namespace = "version" if %w( -v --version ).include? namespace
+ def invoke(full_namespace, args = [], **config)
+ namespace = full_namespace = full_namespace.to_s
+
+ if char = namespace =~ /:(\w+)$/
+ command_name, namespace = $1, namespace.slice(0, char)
+ else
+ command_name = namespace
+ end
+
+ command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
+ command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
- if command = find_by_namespace(namespace)
- command.perform(namespace, args, config)
+ command = find_by_namespace(namespace, command_name)
+ if command && command.all_commands[command_name]
+ command.perform(command_name, args, config)
else
- find_by_namespace("rake").perform(namespace, args, config)
+ find_by_namespace("rake").perform(full_namespace, args, config)
end
end
- # Rails finds namespaces similar to thor, it only adds one rule:
+ # Rails finds namespaces similar to Thor, it only adds one rule:
#
# Command names must end with "_command.rb". This is required because Rails
# looks in load paths and loads the command just before it's going to be used.
@@ -50,8 +60,10 @@ module Rails
#
# Notice that "rails:commands:webrat" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace.
- def find_by_namespace(name) # :nodoc:
- lookups = [ name, "rails:#{name}" ]
+ def find_by_namespace(namespace, command_name = nil) # :nodoc:
+ lookups = [ namespace ]
+ lookups << "#{namespace}:#{command_name}" if command_name
+ lookups.concat lookups.map { |lookup| "rails:#{lookup}" }
lookup(lookups)
@@ -82,16 +94,16 @@ module Rails
[[ "rails", rails ]] + groups.sort.to_a
end
- protected
- def command_type
+ private
+ def command_type # :doc:
@command_type ||= "command"
end
- def lookup_paths
+ def lookup_paths # :doc:
@lookup_paths ||= %w( rails/commands commands )
end
- def file_lookup_paths
+ def file_lookup_paths # :doc:
@file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ]
end
end
diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb
index 31b656ec31..8fda1c87c6 100644
--- a/railties/lib/rails/command/actions.rb
+++ b/railties/lib/rails/command/actions.rb
@@ -8,11 +8,16 @@ module Rails
Dir.chdir(File.expand_path("../../", APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end
- if defined?(ENGINE_PATH)
- def require_application_and_environment!
- require ENGINE_PATH
+ def require_application_and_environment!
+ require ENGINE_PATH if defined?(ENGINE_PATH)
+
+ if defined?(APP_PATH)
+ require APP_PATH
+ Rails.application.require_environment!
end
+ end
+ if defined?(ENGINE_PATH)
def load_tasks
Rake.application.init("rails")
Rake.application.load_rakefile
@@ -24,11 +29,6 @@ module Rails
engine.load_generators
end
else
- def require_application_and_environment!
- require APP_PATH
- Rails.application.require_environment!
- end
-
def load_tasks
Rails.application.load_tasks
end
diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb
index 1efcd69e63..4f074df473 100644
--- a/railties/lib/rails/command/base.rb
+++ b/railties/lib/rails/command/base.rb
@@ -22,7 +22,7 @@ module Rails
# Tries to get the description from a USAGE file one folder above the command
# root.
- def desc(usage = nil, description = nil)
+ def desc(usage = nil, description = nil, options = {})
if usage
super
else
@@ -56,13 +56,15 @@ module Rails
end
def perform(command, args, config) # :nodoc:
- command = nil if Thor::HELP_MAPPINGS.include?(args.first)
+ if Rails::Command::HELP_MAPPINGS.include?(args.first)
+ command, args = "help", []
+ end
dispatch(command, args.dup, nil, config)
end
def printing_commands
- namespace.sub(/^rails:/, "")
+ namespaced_commands
end
def executable
@@ -111,7 +113,7 @@ module Rails
# For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb`
# would return `rails/test`.
def default_command_root
- path = File.expand_path(File.join("../commands", command_name), __dir__)
+ path = File.expand_path(File.join("../commands", command_root_namespace), __dir__)
path if File.exist?(path)
end
@@ -129,6 +131,24 @@ module Rails
super
end
end
+
+ def command_root_namespace
+ (namespace.split(":") - %w( rails )).first
+ end
+
+ def namespaced_commands
+ commands.keys.map do |key|
+ key == command_root_namespace ? key : "#{command_root_namespace}:#{key}"
+ end
+ end
+ end
+
+ def help
+ if command_name = self.class.command_name
+ self.class.command_help(shell, command_name)
+ else
+ super
+ end
end
end
end
diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb
index 2e8517070c..4a92f72f16 100644
--- a/railties/lib/rails/command/behavior.rb
+++ b/railties/lib/rails/command/behavior.rb
@@ -16,13 +16,13 @@ module Rails
@subclasses ||= []
end
- protected
+ 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)
+ def levenshtein_distance(str1, str2) # :doc:
s = str1
t = str2
n = s.length
@@ -58,7 +58,7 @@ module Rails
end
# Prints a list of generators.
- def print_list(base, namespaces) #:nodoc:
+ def print_list(base, namespaces)
return if namespaces.empty?
puts "#{base.camelize}:"
@@ -71,7 +71,7 @@ module Rails
# Receives namespaces in an array and tries to find matching generators
# in the load path.
- def lookup(namespaces) #:nodoc:
+ def lookup(namespaces)
paths = namespaces_to_paths(namespaces)
paths.each do |raw_path|
@@ -91,7 +91,7 @@ module Rails
end
# This will try to load any command in the load path to show in help.
- def lookup! #:nodoc:
+ def lookup!
$LOAD_PATH.each do |base|
Dir[File.join(base, *file_lookup_paths)].each do |path|
begin
@@ -107,7 +107,7 @@ module Rails
# Convert namespaces to paths by replacing ":" for "/" and adding
# an extra lookup. For example, "rails:model" should be searched
# in both: "rails/model/model_generator" and "rails/model_generator".
- def namespaces_to_paths(namespaces) #:nodoc:
+ def namespaces_to_paths(namespaces)
paths = []
namespaces.each do |namespace|
pieces = namespace.split(":")
diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
index 35e8673215..588fb06b15 100644
--- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
+++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
@@ -99,14 +99,14 @@ module Rails
Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment
end
- protected
- def configurations
+ private
+ def configurations # :doc:
require APP_PATH
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Base.configurations
end
- def find_cmd_and_exec(commands, *args)
+ def find_cmd_and_exec(commands, *args) # :doc:
commands = Array(commands)
dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb
index 5b552b2070..281732a936 100644
--- a/railties/lib/rails/commands/destroy/destroy_command.rb
+++ b/railties/lib/rails/commands/destroy/destroy_command.rb
@@ -3,8 +3,13 @@ require "rails/generators"
module Rails
module Command
class DestroyCommand < Base # :nodoc:
- def help
- Rails::Generators.help self.class.command_name
+ no_commands do
+ def help
+ require_application_and_environment!
+ load_generators
+
+ Rails::Generators.help self.class.command_name
+ end
end
def perform(*)
@@ -12,9 +17,9 @@ module Rails
return help unless generator
require_application_and_environment!
- Rails.application.load_generators
+ load_generators
- Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails.root
+ Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails::Command.root
end
end
end
diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb
index 59b2febc43..9dd7ad1012 100644
--- a/railties/lib/rails/commands/generate/generate_command.rb
+++ b/railties/lib/rails/commands/generate/generate_command.rb
@@ -3,8 +3,13 @@ require "rails/generators"
module Rails
module Command
class GenerateCommand < Base # :nodoc:
- def help
- Rails::Generators.help self.class.command_name
+ no_commands do
+ def help
+ require_application_and_environment!
+ load_generators
+
+ Rails::Generators.help self.class.command_name
+ end
end
def perform(*)
@@ -14,6 +19,8 @@ module Rails
require_application_and_environment!
load_generators
+ ARGV.shift
+
Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root
end
end
diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE
index 348f41861f..c5f8ab72bb 100644
--- a/railties/lib/rails/commands/help/USAGE
+++ b/railties/lib/rails/commands/help/USAGE
@@ -1,16 +1,3 @@
-Usage: bin/rails COMMAND [args] [options]
-<% if engine? %>
-The common Rails commands available for engines are:
- generate Generate new code (short-cut alias: "g")
- destroy Undo code generated with "generate" (short-cut alias: "d")
- test Run tests (short-cut alias: "t")
-
-All commands can be run with -h for more information.
-
-If you want to run any commands that need to be run in context
-of the application, like `bin/rails server` or `bin/rails console`,
-you should do it from the application's directory (typically test/dummy).
-<% else %>
The most common rails commands are:
generate Generate new code (short-cut alias: "g")
console Start the Rails console (short-cut alias: "c")
@@ -18,10 +5,11 @@ The most common rails commands are:
test Run tests (short-cut alias: "t")
dbconsole Start a console for the database specified in config/database.yml
(short-cut alias: "db")
+<% unless engine? %>
new Create a new Rails application. "rails new my_app" creates a
new application called MyApp in "./my_app"
+<% end %>
All commands can be run with -h (or --help) for more information.
-<% end %>
In addition to those commands, there are:
diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb
index 74d1fa5021..207dd5d995 100644
--- a/railties/lib/rails/commands/new/new_command.rb
+++ b/railties/lib/rails/commands/new/new_command.rb
@@ -1,8 +1,10 @@
module Rails
module Command
class NewCommand < Base # :nodoc:
- def help
- Rails::Command.invoke :application, [ "--help" ]
+ no_commands do
+ def help
+ Rails::Command.invoke :application, [ "--help" ]
+ end
end
def perform(*)
diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb
index 16587ce067..b40ab006af 100644
--- a/railties/lib/rails/commands/plugin/plugin_command.rb
+++ b/railties/lib/rails/commands/plugin/plugin_command.rb
@@ -11,7 +11,7 @@ module Rails
"#{executable} new [options]"
end
- class_option :rc, type: :boolean, default: File.join("~", ".railsrc"),
+ class_option :rc, type: :string, default: File.join("~", ".railsrc"),
desc: "Initialize the plugin command with previous defaults. Uses .railsrc in your home directory by default."
class_option :no_rc, desc: "Skip evaluating .railsrc."
diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb
index f03dc81117..075b1fd23d 100644
--- a/railties/lib/rails/commands/rake/rake_command.rb
+++ b/railties/lib/rails/commands/rake/rake_command.rb
@@ -28,9 +28,7 @@ module Rails
return @rake_tasks if defined?(@rake_tasks)
- ActiveSupport::Deprecation.silence do
- require_application_and_environment!
- end
+ require_application_and_environment!
Rake::TaskManager.record_task_metadata = true
Rake.application.instance_variable_set(:@name, "rails")
diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE
index dc47a35ff3..b2a6e8493d 100644
--- a/railties/lib/rails/commands/runner/USAGE
+++ b/railties/lib/rails/commands/runner/USAGE
@@ -8,7 +8,7 @@ Run the Ruby file located at `path/to/filename.rb` after loading the app:
<%= executable %> path/to/filename.rb
-<% if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ %>
+<% unless Gem.win_platform? %>
You can also use the runner command as a shebang line for your executables:
#!/usr/bin/env <%= File.expand_path(executable) %>
diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb
index 4989a7837d..6864a9726b 100644
--- a/railties/lib/rails/commands/runner/runner_command.rb
+++ b/railties/lib/rails/commands/runner/runner_command.rb
@@ -5,16 +5,18 @@ module Rails
default: Rails::Command.environment.dup,
desc: "The environment for the runner to operate under (test/development/production)"
- def help
- super
- puts self.class.desc
+ no_commands do
+ def help
+ super
+ puts self.class.desc
+ end
end
def self.banner(*)
"#{super} [<'Some.ruby(code)'> | <filename.rb>]"
end
- def perform(code_or_file = nil, *file_argv)
+ def perform(code_or_file = nil, *command_argv)
unless code_or_file
help
exit 1
@@ -25,9 +27,10 @@ module Rails
require_application_and_environment!
Rails.application.load_runner
+ ARGV.replace(command_argv)
+
if File.exist?(code_or_file)
$0 = code_or_file
- ARGV.replace(file_argv)
Kernel.load code_or_file
else
begin
diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE
new file mode 100644
index 0000000000..96e322fe91
--- /dev/null
+++ b/railties/lib/rails/commands/secrets/USAGE
@@ -0,0 +1,60 @@
+=== Storing Encrypted Secrets in Source Control
+
+The Rails `secrets` commands helps encrypting secrets to slim a production
+environment's `ENV` hash. It's also useful for atomic deploys: no need to
+coordinate key changes to get everything working as the keys are shipped
+with the code.
+
+=== Setup
+
+Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key`
+and `config/secrets.yml.enc` files.
+
+The latter contains all the keys to be encrypted while the former holds the
+encryption key.
+
+Don't lose the key! Put it in a password manager your team can access.
+Should you lose it no one, including you, will be able to access any encrypted
+secrets.
+Don't commit the key! Add `config/secrets.yml.key` to your source control's
+ignore file. If you use Git, Rails handles this for you.
+
+Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to
+manage.
+
+You could prepend that to your server's start command like this:
+
+ RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start
+
+
+The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`:
+
+ production:
+ secret_key_base: so-secret-very-hidden-wow
+ payment_processing_gateway_key: much-safe-very-gaedwey-wow
+
+But that's where the similarities between `secrets.yml` and `secrets.yml.enc`
+end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and
+be encrypted.
+
+A `shared:` top level key is also supported such that any keys there is merged
+into the other environments.
+
+Additionally, Rails won't read encrypted secrets out of the box even if you have
+the key. Add this:
+
+ config.read_encrypted_secrets = true
+
+to the environment you'd like to read encrypted secrets. `bin/rails secrets:setup`
+inserts this into the production environment by default.
+
+=== Editing Secrets
+
+After `bin/rails secrets:setup`, run `bin/rails secrets:edit`.
+
+That command opens a temporary file in `$EDITOR` with the decrypted contents of
+`config/secrets.yml.enc` to edit the encrypted secrets.
+
+When the temporary file is next saved the contents are encrypted and written to
+`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets
+from leaking.
diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb
new file mode 100644
index 0000000000..03a640bd65
--- /dev/null
+++ b/railties/lib/rails/commands/secrets/secrets_command.rb
@@ -0,0 +1,49 @@
+require "active_support"
+require "rails/secrets"
+
+module Rails
+ module Command
+ class SecretsCommand < Rails::Command::Base # :nodoc:
+ no_commands do
+ def help
+ say "Usage:\n #{self.class.banner}"
+ say ""
+ say self.class.desc
+ end
+ end
+
+ def setup
+ require "rails/generators"
+ require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator"
+
+ Rails::Generators::EncryptedSecretsGenerator.start
+ end
+
+ def edit
+ if ENV["EDITOR"].to_s.empty?
+ say "No $EDITOR to open decrypted secrets in. Assign one like this:"
+ say ""
+ say %(EDITOR="mate --wait" bin/rails secrets:edit)
+ say ""
+ say "For editors that fork and exit immediately, it's important to pass a wait flag,"
+ say "otherwise the secrets will be saved immediately with no chance to edit."
+
+ return
+ end
+
+ require_application_and_environment!
+
+ Rails::Secrets.read_for_editing do |tmp_path|
+ say "Waiting for secrets file to be saved. Abort with Ctrl-C."
+ system("\$EDITOR #{tmp_path}")
+ end
+
+ say "New secrets encrypted and saved."
+ rescue Interrupt
+ say "Aborted changing encrypted secrets: nothing saved."
+ rescue Rails::Secrets::MissingKeyError => error
+ say error.message
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb
index e9538b804c..278fe63c51 100644
--- a/railties/lib/rails/commands/server/server_command.rb
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -7,51 +7,14 @@ require "rails/dev_caching"
module Rails
class Server < ::Rack::Server
class Options
- DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze
-
def parse!(args)
- args, options = args.dup, {}
-
- option_parser(options).parse! args
-
- options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
- options[:server] = args.shift
- options
- end
-
- def option_parser(options) # :nodoc:
- OptionParser.new do |opts|
- opts.banner = "Usage: rails server [puma, thin etc] [options]"
-
- opts.separator ""
- opts.separator "Options:"
-
- opts.on("-p", "--port=port", Integer,
- "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
- opts.on("-b", "--binding=IP", String,
- "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v }
- opts.on("-c", "--config=file", String,
- "Uses a custom rackup configuration.") { |v| options[:config] = v }
- opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true }
- opts.on("-e", "--environment=name", String,
- "Specifies the environment to run this server under (test/development/production).",
- "Default: development") { |v| options[:environment] = v }
- opts.on("-P", "--pid=pid", String,
- "Specifies the PID file.",
- "Default: tmp/pids/server.pid") { |v| options[:pid] = v }
- opts.on("-C", "--[no-]dev-caching",
- "Specifies whether to perform caching in development.",
- "true or false") { |v| options[:caching] = v }
-
- opts.separator ""
-
- opts.on("-h", "--help", "Shows this help message.") { puts opts; exit }
- end
+ Rails::Command::ServerCommand.new([], args).server_options
end
end
- def initialize(*)
- super
+ def initialize(options = nil)
+ @default_options = options || {}
+ super(@default_options)
set_environment
end
@@ -90,15 +53,7 @@ module Rails
end
def default_options
- super.merge(
- Port: ENV.fetch("PORT", 3000).to_i,
- Host: ENV.fetch("HOST", "localhost").dup,
- DoNotReverseLookup: true,
- environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup,
- daemonize: false,
- caching: nil,
- pid: Options::DEFAULT_PID_PATH,
- restart_cmd: restart_command)
+ super.merge(@default_options)
end
private
@@ -140,14 +95,34 @@ module Rails
module Command
class ServerCommand < Base # :nodoc:
- def help
- puts Rails::Server::Options.new.option_parser(Hash.new)
+ DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze
+
+ class_option :port, aliases: "-p", type: :numeric,
+ desc: "Runs Rails on the specified port.", banner: :port, default: 3000
+ class_option :binding, aliases: "-b", type: :string,
+ desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.",
+ banner: :IP
+ class_option :config, aliases: "-c", type: :string, default: "config.ru",
+ desc: "Uses a custom rackup configuration.", banner: :file
+ class_option :daemon, aliases: "-d", type: :boolean, default: false,
+ 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 :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."
+
+ def initialize(args = [], local_options = {}, config = {})
+ @original_options = local_options
+ super
+ @server = self.args.shift
+ @log_stdout = options[:daemon].blank? && (options[:environment] || Rails.env) == "development"
end
def perform
set_application_directory!
-
- Rails::Server.new.tap do |server|
+ Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH
@@ -155,6 +130,87 @@ module Rails
server.start
end
end
+
+ no_commands do
+ def server_options
+ {
+ user_supplied_options: user_supplied_options,
+ server: @server,
+ log_stdout: @log_stdout,
+ Port: port,
+ Host: host,
+ DoNotReverseLookup: true,
+ config: options[:config],
+ environment: environment,
+ daemonize: options[:daemon],
+ pid: pid,
+ caching: options["dev-caching"],
+ restart_cmd: restart_command
+ }
+ end
+ end
+
+ private
+ def user_supplied_options
+ @user_supplied_options ||= begin
+ # Convert incoming options array to a hash of flags
+ # ["-p", "3001", "-c", "foo"] # => {"-p" => true, "-c" => true}
+ user_flag = {}
+ @original_options.each_with_index { |command, i| user_flag[command] = true if i.even? }
+
+ # Collect all options that the user has explicitly defined so we can
+ # differentiate them from defaults
+ user_supplied_options = []
+ self.class.class_options.select do |key, option|
+ if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"]
+ name = option.name.to_sym
+ case name
+ when :port
+ name = :Port
+ when :binding
+ name = :Host
+ when :"dev-caching"
+ name = :caching
+ when :daemonize
+ name = :daemon
+ end
+ user_supplied_options << name
+ end
+ end
+ user_supplied_options << :Host if ENV["HOST"]
+ user_supplied_options << :Port if ENV["PORT"]
+ user_supplied_options.uniq
+ end
+ end
+
+ def port
+ ENV.fetch("PORT", options[:port]).to_i
+ end
+
+ def host
+ if options[:binding]
+ options[:binding]
+ else
+ default_host = environment == "development" ? "localhost" : "0.0.0.0"
+ ENV.fetch("HOST", default_host)
+ end
+ end
+
+ def environment
+ options[:environment] || Rails::Command.environment
+ end
+
+ def restart_command
+ "bin/rails server #{@server} #{@original_options.join(" ")}"
+ end
+
+ def pid
+ File.expand_path(options[:pid])
+ end
+
+ def self.banner(*)
+ "rails server [puma, thin etc] [options]"
+ 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 7bf8f61137..65e16900ba 100644
--- a/railties/lib/rails/commands/test/test_command.rb
+++ b/railties/lib/rails/commands/test/test_command.rb
@@ -4,14 +4,16 @@ require "rails/test_unit/minitest_plugin"
module Rails
module Command
class TestCommand < Base # :nodoc:
- def help
- perform # Hand over help printing to minitest.
+ no_commands do
+ def help
+ perform # Hand over help printing to minitest.
+ end
end
def perform(*)
$LOAD_PATH << Rails::Command.root.join("test")
- Minitest.run_via[:rails] = true
+ Minitest.run_via = :rails
require "active_support/testing/autorun"
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index e56f6159ad..dc0b158bd4 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -109,7 +109,7 @@ module Rails
#
# == Endpoint
#
- # An engine can also be a rack application. It can be useful if you have a rack application that
+ # An engine can also be a Rack application. It can be useful if you have a Rack application that
# you would like to wrap with +Engine+ and provide with some of the +Engine+'s features.
#
# To do that, use the +endpoint+ method:
@@ -128,7 +128,7 @@ module Rails
#
# == Middleware stack
#
- # As an engine can now be a rack endpoint, it can also have a middleware
+ # As an engine can now be a Rack endpoint, it can also have a middleware
# stack. The usage is exactly the same as in <tt>Application</tt>:
#
# module MyEngine
@@ -380,7 +380,7 @@ module Rails
def isolate_namespace(mod)
engine_name(generate_railtie_name(mod.name))
- self.routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) }
+ routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) }
self.isolated = true
unless mod.respond_to?(:railtie_namespace)
@@ -499,7 +499,7 @@ module Rails
paths["app/helpers"].existent
end
- # Returns the underlying rack application for this engine.
+ # Returns the underlying Rack application for this engine.
def app
@app || @app_build_lock.synchronize {
@app ||= begin
@@ -549,7 +549,7 @@ module Rails
load(seed_file) if seed_file
end
- # Add configured load paths to ruby load paths and remove duplicates.
+ # Add configured load paths to Ruby's load path, and remove duplicate entries.
initializer :set_load_path, before: :bootstrap_hook do
_all_load_paths.reverse_each do |path|
$LOAD_PATH.unshift(path) if File.directory?(path)
@@ -573,7 +573,7 @@ module Rails
end
initializer :add_routing_paths do |app|
- routing_paths = self.paths["config/routes.rb"].existent
+ routing_paths = paths["config/routes.rb"].existent
if routes? || routing_paths.any?
app.routes_reloader.paths.unshift(*routing_paths)
@@ -643,23 +643,24 @@ module Rails
protected
- def load_config_initializer(initializer)
- ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
- load(initializer)
- end
- end
-
def run_tasks_blocks(*) #:nodoc:
super
paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
- def has_migrations? #:nodoc:
+ private
+
+ def load_config_initializer(initializer) # :doc:
+ ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
+ load(initializer)
+ end
+ end
+
+ def has_migrations?
paths["db/migrate"].existent.any?
end
def self.find_root_with_flag(flag, root_path, default = nil) #:nodoc:
-
while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}")
parent = File.dirname(root_path)
root_path = parent != root_path && parent
@@ -671,24 +672,22 @@ module Rails
Pathname.new File.realpath root
end
- def default_middleware_stack #:nodoc:
+ def default_middleware_stack
ActionDispatch::MiddlewareStack.new
end
- def _all_autoload_once_paths #:nodoc:
+ def _all_autoload_once_paths
config.autoload_once_paths
end
- def _all_autoload_paths #:nodoc:
+ def _all_autoload_paths
@_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
end
- def _all_load_paths #:nodoc:
+ def _all_load_paths
@_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq
end
- private
-
def build_request(env)
env.merge!(env_config)
req = ActionDispatch::Request.new env
diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb
index a23ae44b0b..b9ef63243a 100644
--- a/railties/lib/rails/engine/commands.rb
+++ b/railties/lib/rails/engine/commands.rb
@@ -1,12 +1,7 @@
-require "rails/command"
+unless defined?(APP_PATH)
+ if File.exist?(File.expand_path("test/dummy/config/application.rb", ENGINE_ROOT))
+ APP_PATH = File.expand_path("test/dummy/config/application", ENGINE_ROOT)
+ end
+end
-aliases = {
- "g" => "generate",
- "d" => "destroy",
- "t" => "test"
-}
-
-command = ARGV.shift
-command = aliases[command] || command
-
-Rails::Command.invoke command, ARGV
+require "rails/commands"
diff --git a/railties/lib/rails/engine/updater.rb b/railties/lib/rails/engine/updater.rb
new file mode 100644
index 0000000000..2ecf994a5c
--- /dev/null
+++ b/railties/lib/rails/engine/updater.rb
@@ -0,0 +1,19 @@
+require "rails/generators"
+require "rails/generators/rails/plugin/plugin_generator"
+
+module Rails
+ class Engine
+ class Updater
+ class << self
+ def generator
+ @generator ||= Rails::Generators::PluginGenerator.new ["plugin"],
+ { engine: true }, destination_root: ENGINE_ROOT
+ end
+
+ def run(action)
+ generator.send(action)
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb
index 9c49e0655a..7bacf2e0ba 100644
--- a/railties/lib/rails/gem_version.rb
+++ b/railties/lib/rails/gem_version.rb
@@ -6,7 +6,7 @@ module Rails
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index 67037106e5..8ec805370b 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -62,251 +62,255 @@ module Rails
stylesheets: true,
stylesheet_engine: :css,
scaffold_stylesheet: true,
- test_framework: false,
+ system_tests: nil,
+ test_framework: nil,
template_engine: :erb
}
}
- def self.configure!(config) #:nodoc:
- api_only! if config.api_only
- no_color! unless config.colorize_logging
- aliases.deep_merge! config.aliases
- options.deep_merge! config.options
- fallbacks.merge! config.fallbacks
- templates_path.concat config.templates
- templates_path.uniq!
- hide_namespaces(*config.hidden_namespaces)
- end
+ class << self
+ def configure!(config) #:nodoc:
+ api_only! if config.api_only
+ no_color! unless config.colorize_logging
+ aliases.deep_merge! config.aliases
+ options.deep_merge! config.options
+ fallbacks.merge! config.fallbacks
+ templates_path.concat config.templates
+ templates_path.uniq!
+ hide_namespaces(*config.hidden_namespaces)
+ end
- def self.templates_path #:nodoc:
- @templates_path ||= []
- end
+ def templates_path #:nodoc:
+ @templates_path ||= []
+ end
- def self.aliases #:nodoc:
- @aliases ||= DEFAULT_ALIASES.dup
- end
+ def aliases #:nodoc:
+ @aliases ||= DEFAULT_ALIASES.dup
+ end
- def self.options #:nodoc:
- @options ||= DEFAULT_OPTIONS.dup
- end
+ def options #:nodoc:
+ @options ||= DEFAULT_OPTIONS.dup
+ end
- # Hold configured generators fallbacks. If a plugin developer wants a
- # generator group to fallback to another group in case of missing generators,
- # they can add a fallback.
- #
- # For example, shoulda is considered a test_framework and is an extension
- # of test_unit. However, most part of shoulda generators are similar to
- # test_unit ones.
- #
- # Shoulda then can tell generators to search for test_unit generators when
- # some of them are not available by adding a fallback:
- #
- # Rails::Generators.fallbacks[:shoulda] = :test_unit
- def self.fallbacks
- @fallbacks ||= {}
- end
+ # Hold configured generators fallbacks. If a plugin developer wants a
+ # generator group to fallback to another group in case of missing generators,
+ # they can add a fallback.
+ #
+ # For example, shoulda is considered a test_framework and is an extension
+ # of test_unit. However, most part of shoulda generators are similar to
+ # test_unit ones.
+ #
+ # Shoulda then can tell generators to search for test_unit generators when
+ # some of them are not available by adding a fallback:
+ #
+ # Rails::Generators.fallbacks[:shoulda] = :test_unit
+ def fallbacks
+ @fallbacks ||= {}
+ end
- # Configure generators for API only applications. It basically hides
- # everything that is usually browser related, such as assets and session
- # migration generators, and completely disable helpers and assets
- # so generators such as scaffold won't create them.
- def self.api_only!
- hide_namespaces "assets", "helper", "css", "js"
-
- options[:rails].merge!(
- api: true,
- assets: false,
- helper: false,
- template_engine: nil
- )
-
- if ARGV.first == "mailer"
- options[:rails].merge!(template_engine: :erb)
+ # Configure generators for API only applications. It basically hides
+ # everything that is usually browser related, such as assets and session
+ # migration generators, and completely disable helpers and assets
+ # so generators such as scaffold won't create them.
+ def api_only!
+ hide_namespaces "assets", "helper", "css", "js"
+
+ options[:rails].merge!(
+ api: true,
+ assets: false,
+ helper: false,
+ template_engine: nil
+ )
+
+ if ARGV.first == "mailer"
+ options[:rails].merge!(template_engine: :erb)
+ end
end
- end
- # Remove the color from output.
- def self.no_color!
- Thor::Base.shell = Thor::Shell::Basic
- end
+ # Remove the color from output.
+ def no_color!
+ Thor::Base.shell = Thor::Shell::Basic
+ end
- # Returns an array of generator namespaces that are hidden.
- # Generator namespaces may be hidden for a variety of reasons.
- # Some are aliased such as "rails:migration" and can be
- # invoked with the shorter "migration", others are private to other generators
- # such as "css:scaffold".
- def self.hidden_namespaces
- @hidden_namespaces ||= begin
- orm = options[:rails][:orm]
- test = options[:rails][:test_framework]
- template = options[:rails][:template_engine]
- css = options[:rails][:stylesheet_engine]
-
- [
- "rails",
- "resource_route",
- "#{orm}:migration",
- "#{orm}:model",
- "#{test}:controller",
- "#{test}:helper",
- "#{test}:integration",
- "#{test}:mailer",
- "#{test}:model",
- "#{test}:scaffold",
- "#{test}:view",
- "#{test}:job",
- "#{template}:controller",
- "#{template}:scaffold",
- "#{template}:mailer",
- "#{css}:scaffold",
- "#{css}:assets",
- "css:assets",
- "css:scaffold"
- ]
+ # Returns an array of generator namespaces that are hidden.
+ # Generator namespaces may be hidden for a variety of reasons.
+ # Some are aliased such as "rails:migration" and can be
+ # invoked with the shorter "migration", others are private to other generators
+ # such as "css:scaffold".
+ def hidden_namespaces
+ @hidden_namespaces ||= begin
+ orm = options[:rails][:orm]
+ test = options[:rails][:test_framework]
+ template = options[:rails][:template_engine]
+ css = options[:rails][:stylesheet_engine]
+
+ [
+ "rails",
+ "resource_route",
+ "#{orm}:migration",
+ "#{orm}:model",
+ "#{test}:controller",
+ "#{test}:helper",
+ "#{test}:integration",
+ "#{test}:system",
+ "#{test}:mailer",
+ "#{test}:model",
+ "#{test}:scaffold",
+ "#{test}:view",
+ "#{test}:job",
+ "#{template}:controller",
+ "#{template}:scaffold",
+ "#{template}:mailer",
+ "#{css}:scaffold",
+ "#{css}:assets",
+ "css:assets",
+ "css:scaffold"
+ ]
+ end
end
- end
- class << self
def hide_namespaces(*namespaces)
hidden_namespaces.concat(namespaces)
end
alias hide_namespace hide_namespaces
- end
- # Show help message with available generators.
- def self.help(command = "generate")
- puts "Usage: rails #{command} GENERATOR [args] [options]"
- puts
- puts "General options:"
- puts " -h, [--help] # Print generator's options and usage"
- puts " -p, [--pretend] # Run but do not make any changes"
- puts " -f, [--force] # Overwrite files that already exist"
- puts " -s, [--skip] # Skip files that already exist"
- puts " -q, [--quiet] # Suppress status output"
- puts
- puts "Please choose a generator below."
- puts
-
- print_generators
- end
-
- def self.public_namespaces
- lookup!
- subclasses.map(&:namespace)
- end
-
- def self.print_generators
- sorted_groups.each { |b, n| print_list(b, n) }
- end
+ # Show help message with available generators.
+ def help(command = "generate")
+ puts "Usage: rails #{command} GENERATOR [args] [options]"
+ puts
+ puts "General options:"
+ puts " -h, [--help] # Print generator's options and usage"
+ puts " -p, [--pretend] # Run but do not make any changes"
+ puts " -f, [--force] # Overwrite files that already exist"
+ puts " -s, [--skip] # Skip files that already exist"
+ puts " -q, [--quiet] # Suppress status output"
+ puts
+ puts "Please choose a generator below."
+ puts
+
+ print_generators
+ end
- def self.sorted_groups
- namespaces = public_namespaces
- namespaces.sort!
+ def public_namespaces
+ lookup!
+ subclasses.map(&:namespace)
+ end
- groups = Hash.new { |h, k| h[k] = [] }
- namespaces.each do |namespace|
- base = namespace.split(":").first
- groups[base] << namespace
+ def print_generators
+ sorted_groups.each { |b, n| print_list(b, n) }
end
- rails = groups.delete("rails")
- rails.map! { |n| n.sub(/^rails:/, "") }
- rails.delete("app")
- rails.delete("plugin")
+ def sorted_groups
+ namespaces = public_namespaces
+ namespaces.sort!
- hidden_namespaces.each { |n| groups.delete(n.to_s) }
+ groups = Hash.new { |h, k| h[k] = [] }
+ namespaces.each do |namespace|
+ base = namespace.split(":").first
+ groups[base] << namespace
+ end
- [[ "rails", rails ]] + groups.sort.to_a
- end
+ rails = groups.delete("rails")
+ rails.map! { |n| n.sub(/^rails:/, "") }
+ rails.delete("app")
+ rails.delete("plugin")
+ rails.delete("encrypted_secrets")
- # Rails finds namespaces similar to thor, it only adds one rule:
- #
- # Generators names must end with "_generator.rb". This is required because Rails
- # looks in load paths and loads the generator just before it's going to be used.
- #
- # find_by_namespace :webrat, :rails, :integration
- #
- # Will search for the following generators:
- #
- # "rails:webrat", "webrat:integration", "webrat"
- #
- # Notice that "rails:generators:webrat" could be loaded as well, what
- # Rails looks for is the first and last parts of the namespace.
- def self.find_by_namespace(name, base = nil, context = nil) #:nodoc:
- lookups = []
- lookups << "#{base}:#{name}" if base
- lookups << "#{name}:#{context}" if context
-
- unless base || context
- unless name.to_s.include?(?:)
- lookups << "#{name}:#{name}"
- lookups << "rails:#{name}"
- end
- lookups << "#{name}"
+ hidden_namespaces.each { |n| groups.delete(n.to_s) }
+
+ [[ "rails", rails ]] + groups.sort.to_a
end
- lookup(lookups)
+ # Rails finds namespaces similar to Thor, it only adds one rule:
+ #
+ # Generators names must end with "_generator.rb". This is required because Rails
+ # looks in load paths and loads the generator just before it's going to be used.
+ #
+ # find_by_namespace :webrat, :rails, :integration
+ #
+ # Will search for the following generators:
+ #
+ # "rails:webrat", "webrat:integration", "webrat"
+ #
+ # Notice that "rails:generators:webrat" could be loaded as well, what
+ # Rails looks for is the first and last parts of the namespace.
+ def find_by_namespace(name, base = nil, context = nil) #:nodoc:
+ lookups = []
+ lookups << "#{base}:#{name}" if base
+ lookups << "#{name}:#{context}" if context
+
+ unless base || context
+ unless name.to_s.include?(?:)
+ lookups << "#{name}:#{name}"
+ lookups << "rails:#{name}"
+ end
+ lookups << "#{name}"
+ end
- namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
- lookups.each do |namespace|
+ lookup(lookups)
- klass = namespaces[namespace]
- return klass if klass
- end
+ namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
+ lookups.each do |namespace|
- invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
- end
+ klass = namespaces[namespace]
+ return klass if klass
+ end
- # Receives a namespace, arguments and the behavior to invoke the generator.
- # It's used as the default entry point for generate, destroy and update
- # commands.
- def self.invoke(namespace, args = ARGV, config = {})
- names = namespace.to_s.split(":")
- if klass = find_by_namespace(names.pop, names.any? && names.join(":"))
- args << "--help" if args.empty? && klass.arguments.any?(&:required?)
- 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}'. "
- 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
+ invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
end
- end
- protected
- def self.print_list(base, namespaces)
- namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) }
- super
+ # Receives a namespace, arguments and the behavior to invoke the generator.
+ # It's used as the default entry point for generate, destroy and update
+ # commands.
+ def invoke(namespace, args = ARGV, config = {})
+ names = namespace.to_s.split(":")
+ if klass = find_by_namespace(names.pop, names.any? && names.join(":"))
+ args << "--help" if args.empty? && klass.arguments.any?(&:required?)
+ 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}'. "
+ 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
+ end
end
- # Try fallbacks for the given base.
- def self.invoke_fallbacks_for(name, base) #:nodoc:
- return nil unless base && fallbacks[base.to_sym]
- invoked_fallbacks = []
-
- Array(fallbacks[base.to_sym]).each do |fallback|
- next if invoked_fallbacks.include?(fallback)
- invoked_fallbacks << fallback
+ private
- klass = find_by_namespace(name, fallback)
- return klass if klass
+ def print_list(base, namespaces) # :doc:
+ namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) }
+ super
end
- nil
- end
+ # Try fallbacks for the given base.
+ def invoke_fallbacks_for(name, base)
+ return nil unless base && fallbacks[base.to_sym]
+ invoked_fallbacks = []
- def self.command_type
- @command_type ||= "generator"
- end
+ Array(fallbacks[base.to_sym]).each do |fallback|
+ next if invoked_fallbacks.include?(fallback)
+ invoked_fallbacks << fallback
- def self.lookup_paths
- @lookup_paths ||= %w( rails/generators generators )
- end
+ klass = find_by_namespace(name, fallback)
+ return klass if klass
+ end
- def self.file_lookup_paths
- @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ]
- end
+ nil
+ end
+
+ def command_type # :doc:
+ @command_type ||= "generator"
+ end
+
+ def lookup_paths # :doc:
+ @lookup_paths ||= %w( rails/generators generators )
+ end
+
+ def file_lookup_paths # :doc:
+ @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ]
+ end
+ end
end
end
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 5075eb1328..0bd0615b7e 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -260,32 +260,32 @@ module Rails
@after_bundle_callbacks << block
end
- protected
+ private
# Define log for backwards compatibility. If just one argument is sent,
# invoke say, otherwise invoke say_status. Differently from say and
# similarly to say_status, this method respects the quiet? option given.
- def log(*args)
+ def log(*args) # :doc:
if args.size == 1
say args.first.to_s unless options.quiet?
else
- args << (self.behavior == :invoke ? :green : :red)
+ args << (behavior == :invoke ? :green : :red)
say_status(*args)
end
end
# Runs the supplied command using either "rake ..." or "rails ..."
# based on the executor parameter provided.
- def execute_command(executor, command, options = {})
+ def execute_command(executor, command, options = {}) # :doc:
log executor, command
env = options[:env] || ENV["RAILS_ENV"] || "development"
- sudo = options[:sudo] && RbConfig::CONFIG["host_os"] !~ /mswin|mingw/ ? "sudo " : ""
+ sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : ""
in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", verbose: false) }
end
# Add an extension to the given name based on the platform.
- def extify(name)
- if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
+ def extify(name) # :doc:
+ if Gem.win_platform?
"#{name}.bat"
else
name
@@ -294,7 +294,7 @@ module Rails
# Surround string with single quotes if there is no quotes.
# Otherwise fall back to double quotes
- def quote(value)
+ def quote(value) # :doc:
return value.inspect unless value.is_a? String
if value.include?("'")
diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb
index 587c61fd42..d06609e91e 100644
--- a/railties/lib/rails/generators/actions/create_migration.rb
+++ b/railties/lib/rails/generators/actions/create_migration.rb
@@ -37,9 +37,9 @@ module Rails
end
alias :exists? :existing_migration
- protected
+ private
- def on_conflict_behavior
+ def on_conflict_behavior # :doc:
options = base.options.merge(config)
if identical?
say_status :identical, :blue, relative_existing_migration
@@ -54,13 +54,13 @@ module Rails
say_status :skip, :yellow
else
say_status :conflict, :red
- raise Error, "Another migration is already named #{migration_file_name}: " +
- "#{existing_migration}. Use --force to replace this migration " +
+ raise Error, "Another migration is already named #{migration_file_name}: " \
+ "#{existing_migration}. Use --force to replace this migration " \
"or --skip to ignore conflicted file."
end
end
- def say_status(status, color, message = relative_destination)
+ def say_status(status, color, message = relative_destination) # :doc:
base.shell.say_status(status, message, color) if config[:verbose]
end
end
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 83e9c30548..9109be5e04 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -13,6 +13,7 @@ module Rails
DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver )
JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc )
DATABASES.concat(JDBC_DATABASES)
+ WEBPACKS = %w( react vue angular )
attr_accessor :rails_template
add_shebang_option!
@@ -30,15 +31,15 @@ module Rails
class_option :database, type: :string, aliases: "-d", default: "sqlite3",
desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
- class_option :javascript, type: :string, aliases: "-j", default: "jquery",
- desc: "Preconfigure for selected JavaScript library"
+ class_option :webpack, type: :string, default: nil,
+ desc: "Preconfigure for app-like JavaScript with Webpack (options: #{WEBPACKS.join('/')})"
+
+ 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_bundle, type: :boolean, aliases: "-B", default: false,
- desc: "Don't run bundle install"
-
class_option :skip_git, type: :boolean, aliases: "-G", default: false,
desc: "Skip .gitignore file"
@@ -79,6 +80,9 @@ module Rails
class_option :skip_test, type: :boolean, aliases: "-T", default: false,
desc: "Skip test files"
+ class_option :skip_system_test, type: :boolean, default: false,
+ desc: "Skip system test files"
+
class_option :dev, type: :boolean, default: false,
desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
@@ -102,9 +106,9 @@ module Rails
convert_database_option_for_jruby
end
- protected
+ private
- def gemfile_entry(name, *args)
+ def gemfile_entry(name, *args) # :doc:
options = args.extract_options!
version = args.first
github = options[:github]
@@ -120,11 +124,12 @@ module Rails
self
end
- def gemfile_entries
+ def gemfile_entries # :doc:
[rails_gemfile_entry,
database_gemfile_entry,
webserver_gemfile_entry,
assets_gemfile_entry,
+ webpacker_gemfile_entry,
javascript_gemfile_entry,
jbuilder_gemfile_entry,
psych_gemfile_entry,
@@ -132,13 +137,13 @@ module Rails
@extra_entries].flatten.find_all(&@gem_filter)
end
- def add_gem_entry_filter
+ def add_gem_entry_filter # :doc:
@gem_filter = lambda { |next_filter, entry|
yield(entry) && next_filter.call(entry)
}.curry[@gem_filter]
end
- def builder
+ def builder # :doc:
@builder ||= begin
builder_class = get_builder_class
builder_class.include(ActionMethods)
@@ -146,24 +151,24 @@ module Rails
end
end
- def build(meth, *args)
+ def build(meth, *args) # :doc:
builder.send(meth, *args) if builder.respond_to?(meth)
end
- def create_root
+ def create_root # :doc:
valid_const?
empty_directory "."
FileUtils.cd(destination_root) unless options[:pretend]
end
- def apply_rails_template
+ def apply_rails_template # :doc:
apply rails_template if rails_template
rescue Thor::Error, LoadError, Errno::ENOENT => e
raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}"
end
- def set_default_accessors!
+ def set_default_accessors! # :doc:
self.destination_root = File.expand_path(app_path, destination_root)
self.rails_template = \
case options[:template]
@@ -176,32 +181,32 @@ module Rails
end
end
- def database_gemfile_entry
+ def database_gemfile_entry # :doc:
return [] if options[:skip_active_record]
gem_name, gem_version = gem_for_database
GemfileEntry.version gem_name, gem_version,
"Use #{options[:database]} as the database for Active Record"
end
- def webserver_gemfile_entry
+ def webserver_gemfile_entry # :doc:
return [] if options[:skip_puma]
comment = "Use Puma as the app server"
- GemfileEntry.new("puma", "~> 3.0", comment)
+ GemfileEntry.new("puma", "~> 3.7", comment)
end
- def include_all_railties?
+ def include_all_railties? # :doc:
options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets, :skip_action_cable).none?
end
- def comment_if(value)
+ def comment_if(value) # :doc:
options[value] ? "# " : ""
end
- def keeps?
+ def keeps? # :doc:
!options[:skip_keeps]
end
- def sqlite3?
+ def sqlite3? # :doc:
!options[:skip_active_record] && options[:database] == "sqlite3"
end
@@ -239,6 +244,7 @@ module Rails
def rails_gemfile_entry
dev_edge_common = [
+ GemfileEntry.github("arel", "rails/arel"),
]
if options.dev?
[
@@ -256,14 +262,13 @@ module Rails
end
def rails_version_specifier(gem_version = Rails.gem_version)
- if gem_version.prerelease?
- next_series = gem_version
- next_series = next_series.bump while next_series.segments.size > 2
-
- [">= #{gem_version}", "< #{next_series}"]
- elsif gem_version.segments.size == 3
+ if gem_version.segments.size == 3 || gem_version.release.segments.size == 3
+ # ~> 1.2.3
+ # ~> 1.2.3.pre4
"~> #{gem_version}"
else
+ # ~> 1.2.3, >= 1.2.3.4
+ # ~> 1.2.3, >= 1.2.3.4.pre5
patch = gem_version.segments[0, 3].join(".")
["~> #{patch}", ">= #{gem_version}"]
end
@@ -274,7 +279,7 @@ module Rails
case options[:database]
when "mysql" then ["mysql2", [">= 0.3.18", "< 0.5"]]
when "postgresql" then ["pg", ["~> 0.18"]]
- when "oracle" then ["ruby-oci8", nil]
+ when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
when "frontbase" then ["ruby-frontbase", nil]
when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil]
@@ -290,7 +295,6 @@ module Rails
case options[:database]
when "postgresql" then options[:database].replace "jdbcpostgresql"
when "mysql" then options[:database].replace "jdbcmysql"
- when "oracle" then options[:database].replace "jdbc"
when "sqlite3" then options[:database].replace "jdbcsqlite3"
end
end
@@ -312,6 +316,13 @@ module Rails
gems
end
+ def webpacker_gemfile_entry
+ return [] unless options[:webpack]
+
+ comment = "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker"
+ GemfileEntry.new "webpacker", nil, comment
+ end
+
def jbuilder_gemfile_entry
comment = "Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder"
GemfileEntry.new "jbuilder", "~> 2.5", comment, {}, options[:api]
@@ -328,9 +339,6 @@ module Rails
gems = [javascript_runtime_gemfile_entry]
gems << coffee_gemfile_entry unless options[:skip_coffee]
- gems << GemfileEntry.version("#{options[:javascript]}-rails", nil,
- "Use #{options[:javascript]} as the JavaScript library")
-
unless options[:skip_turbolinks]
gems << GemfileEntry.version("turbolinks", "~> 5",
"Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks")
@@ -397,6 +405,10 @@ module Rails
!options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin")
end
+ def depends_on_system_test?
+ !(options[:skip_system_test] || options[:skip_test] || options[:api])
+ end
+
def depend_on_listen?
!options[:skip_listen] && os_supports_listen_out_of_the_box?
end
@@ -409,6 +421,13 @@ module Rails
bundle_command("install") if bundle_install?
end
+ def run_webpack
+ if !(webpack = options[:webpack]).nil?
+ rails_command "webpacker:install"
+ rails_command "webpacker:install:#{webpack}" unless webpack == "webpack"
+ end
+ end
+
def generate_spring_binstubs
if bundle_install? && spring_install?
bundle_command("exec spring binstub --all")
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index c707bbfcbf..a650c52626 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -239,11 +239,11 @@ module Rails
end
end
- protected
+ private
# Check whether the given class names are already taken by user
# application or Ruby on Rails.
- def class_collisions(*class_names) #:nodoc:
+ def class_collisions(*class_names)
return unless behavior == :invoke
class_names.flatten.each do |class_name|
@@ -256,15 +256,15 @@ module Rails
last = extract_last_module(nesting)
if last && last.const_defined?(last_name.camelize, false)
- raise Error, "The name '#{class_name}' is either already used in your application " <<
- "or reserved by Ruby on Rails. Please choose an alternative and run " <<
+ raise Error, "The name '#{class_name}' is either already used in your application " \
+ "or reserved by Ruby on Rails. Please choose an alternative and run " \
"this generator again."
end
end
end
# Takes in an array of nested modules and extracts the last module
- def extract_last_module(nesting)
+ def extract_last_module(nesting) # :doc:
nesting.inject(Object) do |last_module, nest|
break unless last_module.const_defined?(nest, false)
last_module.const_get(nest)
@@ -272,12 +272,12 @@ module Rails
end
# Use Rails default banner.
- def self.banner
+ def self.banner # :doc:
"rails generate #{namespace.sub(/^rails:/, '')} #{arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, " ")
end
# Sets the base_name taking into account the current class namespace.
- def self.base_name
+ def self.base_name # :doc:
@base_name ||= begin
if base = name.to_s.split("::").first
base.underscore
@@ -287,7 +287,7 @@ module Rails
# Removes the namespaces and get the generator name. For example,
# Rails::Generators::ModelGenerator will return "model" as generator name.
- def self.generator_name
+ def self.generator_name # :doc:
@generator_name ||= begin
if generator = name.to_s.split("::").last
generator.sub!(/Generator$/, "")
@@ -298,18 +298,18 @@ module Rails
# Returns the default value for the option name given doing a lookup in
# Rails::Generators.options.
- def self.default_value_for_option(name, options)
+ def self.default_value_for_option(name, options) # :doc:
default_for_option(Rails::Generators.options, name, options, options[:default])
end
# Returns default aliases for the option name given doing a lookup in
# Rails::Generators.aliases.
- def self.default_aliases_for_option(name, options)
+ def self.default_aliases_for_option(name, options) # :doc:
default_for_option(Rails::Generators.aliases, name, options, options[:aliases])
end
# Returns default for the option name given doing a lookup in config.
- def self.default_for_option(config, name, options, default)
+ def self.default_for_option(config, name, options, default) # :doc:
if generator_name && (c = config[generator_name.to_sym]) && c.key?(name)
c[name]
elsif base_name && (c = config[base_name.to_sym]) && c.key?(name)
@@ -331,7 +331,7 @@ module Rails
def self.prepare_for_invocation(name, value) #:nodoc:
return super unless value.is_a?(String) || value.is_a?(Symbol)
- if value && constants = self.hooks[name]
+ if value && constants = hooks[name]
value = name if TrueClass === value
Rails::Generators.find_by_namespace(value, *constants)
elsif klass = Rails::Generators.find_by_namespace(value)
@@ -343,7 +343,7 @@ module Rails
# Small macro to add ruby as an option to the generator with proper
# default value plus an instance helper method called shebang.
- def self.add_shebang_option!
+ def self.add_shebang_option! # :doc:
class_option :ruby, type: :string, aliases: "-r", default: Thor::Util.ruby_command,
desc: "Path to the Ruby binary of your choice", banner: "PATH"
@@ -361,7 +361,7 @@ module Rails
}
end
- def self.usage_path
+ def self.usage_path # :doc:
paths = [
source_root && File.expand_path("../USAGE", source_root),
default_generator_root && File.join(default_generator_root, "USAGE")
@@ -369,7 +369,7 @@ module Rails
paths.compact.detect { |path| File.exist? path }
end
- def self.default_generator_root
+ def self.default_generator_root # :doc:
path = File.expand_path(File.join(base_name, generator_name), base_root)
path if File.exist?(path)
end
diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb
index d01502002f..97d9ab29d4 100644
--- a/railties/lib/rails/generators/erb.rb
+++ b/railties/lib/rails/generators/erb.rb
@@ -3,7 +3,7 @@ require "rails/generators/named_base"
module Erb # :nodoc:
module Generators # :nodoc:
class Base < Rails::Generators::NamedBase #:nodoc:
- protected
+ private
def formats
[format]
@@ -17,8 +17,8 @@ module Erb # :nodoc:
:erb
end
- def filename_with_extensions(name, format = self.format)
- [name, format, handler].compact.join(".")
+ def filename_with_extensions(name, file_format = format)
+ [name, file_format, handler].compact.join(".")
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
index f150240908..3f1d9932f6 100644
--- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
@@ -9,10 +9,10 @@ module Erb # :nodoc:
view_base_path = File.join("app/views", class_path, file_name + "_mailer")
empty_directory view_base_path
- if self.behavior == :invoke
+ if behavior == :invoke
formats.each do |format|
layout_path = File.join("app/views/layouts", class_path, filename_with_extensions("mailer", format))
- template filename_with_extensions(:layout, format), layout_path
+ template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path)
end
end
@@ -26,7 +26,7 @@ module Erb # :nodoc:
end
end
- protected
+ private
def formats
[:text, :html]
diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
index 154d85f381..0d77ef21da 100644
--- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
@@ -21,7 +21,7 @@ module Erb # :nodoc:
end
end
- protected
+ private
def available_views
%w(index edit show new _form)
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
index 519b6c8603..4f2e84f924 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
@@ -1,4 +1,4 @@
-<%%= form_for(<%= singular_table_name %>) do |f| %>
+<%%= form_with(model: <%= singular_table_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>
@@ -14,21 +14,21 @@
<% attributes.each do |attribute| -%>
<div class="field">
<% if attribute.password_digest? -%>
- <%%= f.label :password %>
- <%%= f.password_field :password %>
+ <%%= form.label :password %>
+ <%%= form.password_field :password, id: :<%= field_id(:password) %> %>
</div>
<div class="field">
- <%%= f.label :password_confirmation %>
- <%%= f.password_field :password_confirmation %>
+ <%%= form.label :password_confirmation %>
+ <%%= form.password_field :password_confirmation, id: :<%= field_id(:password_confirmation) %> %>
<% else -%>
- <%%= f.label :<%= attribute.column_name %> %>
- <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %>
+ <%%= form.label :<%= attribute.column_name %> %>
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, id: :<%= field_id(attribute.column_name) %> %>
<% end -%>
</div>
<% end -%>
<div class="actions">
- <%%= f.submit %>
+ <%%= form.submit %>
</div>
<%% end %>
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index 0d63b9a5c9..82481169c3 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -35,7 +35,7 @@ module Rails
end
def set_migration_assigns!(destination)
- destination = File.expand_path(destination, self.destination_root)
+ destination = File.expand_path(destination, destination_root)
migration_dir = File.dirname(destination)
@migration_number = self.class.next_migration_number(migration_dir)
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index 45f2fba5b9..46001f306a 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -32,145 +32,153 @@ 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
+
# FIXME: We are avoiding to use alias because a bug on thor that make
# this method public and add it to the task list.
- def singular_name
+ def singular_name # :doc:
file_name
end
# Wrap block with namespace of current application
# if namespace exists and is not skipped
- def module_namespacing(&block)
+ def module_namespacing(&block) # :doc:
content = capture(&block)
content = wrap_with_namespace(content) if namespaced?
concat(content)
end
- def indent(content, multiplier = 2)
+ def indent(content, multiplier = 2) # :doc:
spaces = " " * multiplier
content.each_line.map { |line| line.blank? ? line : "#{spaces}#{line}" }.join
end
- def wrap_with_namespace(content)
+ def wrap_with_namespace(content) # :doc:
content = indent(content).chomp
"module #{namespace.name}\n#{content}\nend\n"
end
- def inside_template
+ def inside_template # :doc:
@inside_template = true
yield
ensure
@inside_template = false
end
- def inside_template?
+ def inside_template? # :doc:
@inside_template
end
- def namespace
+ def namespace # :doc:
Rails::Generators.namespace
end
- def namespaced?
+ def namespaced? # :doc:
!options[:skip_namespace] && namespace
end
- def file_path
+ def namespace_dirs
+ @namespace_dirs ||= namespace.name.split("::").map(&:underscore)
+ end
+
+ def file_path # :doc:
@file_path ||= (class_path + [file_name]).join("/")
end
- def class_path
+ def class_path # :doc:
inside_template? || !namespaced? ? regular_class_path : namespaced_class_path
end
- def regular_class_path
+ def regular_class_path # :doc:
@class_path
end
- def namespaced_file_path
- @namespaced_file_path ||= namespaced_class_path.join("/")
+ def namespaced_class_path # :doc:
+ @namespaced_class_path ||= namespace_dirs + @class_path
end
- def namespaced_class_path
- @namespaced_class_path ||= [namespaced_path] + @class_path
+ def namespaced_path # :doc:
+ @namespaced_path ||= namespace_dirs.join("/")
end
- def namespaced_path
- @namespaced_path ||= namespace.name.split("::").first.underscore
- end
-
- def class_name
+ def class_name # :doc:
(class_path + [file_name]).map!(&:camelize).join("::")
end
- def human_name
+ def human_name # :doc:
@human_name ||= singular_name.humanize
end
- def plural_name
+ def plural_name # :doc:
@plural_name ||= singular_name.pluralize
end
- def i18n_scope
+ def i18n_scope # :doc:
@i18n_scope ||= file_path.tr("/", ".")
end
- def table_name
+ def table_name # :doc:
@table_name ||= begin
base = pluralize_table_names? ? plural_name : singular_name
(class_path + [base]).join("_")
end
end
- def uncountable?
+ def uncountable? # :doc:
singular_name == plural_name
end
- def index_helper
+ def index_helper # :doc:
uncountable? ? "#{plural_table_name}_index" : plural_table_name
end
- def show_helper
+ def show_helper # :doc:
"#{singular_table_name}_url(@#{singular_table_name})"
end
- def edit_helper
+ def edit_helper # :doc:
"edit_#{show_helper}"
end
- def new_helper
+ def new_helper # :doc:
"new_#{singular_table_name}_url"
end
- def singular_table_name
+ def field_id(attribute_name)
+ [singular_table_name, attribute_name].join('_')
+ end
+
+ def singular_table_name # :doc:
@singular_table_name ||= (pluralize_table_names? ? table_name.singularize : table_name)
end
- def plural_table_name
+ def plural_table_name # :doc:
@plural_table_name ||= (pluralize_table_names? ? table_name : table_name.pluralize)
end
- def plural_file_name
+ def plural_file_name # :doc:
@plural_file_name ||= file_name.pluralize
end
- def fixture_file_name
+ def fixture_file_name # :doc:
@fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name)
end
- def route_url
+ def route_url # :doc:
@route_url ||= class_path.collect { |dname| "/" + dname }.join + "/" + plural_file_name
end
- def url_helper_prefix
+ def url_helper_prefix # :doc:
@url_helper_prefix ||= (class_path + [file_name]).join("_")
end
# Tries to retrieve the application name or simply return application.
- def application_name
+ def application_name # :doc:
if defined?(Rails) && Rails.application
Rails.application.class.name.split("::").first.underscore
else
@@ -178,20 +186,20 @@ module Rails
end
end
- def assign_names!(name) #:nodoc:
+ def assign_names!(name)
@class_path = name.include?("/") ? name.split("/") : name.split("::")
@class_path.map!(&:underscore)
@file_name = @class_path.pop
end
# Convert attributes array into GeneratedAttribute objects.
- def parse_attributes! #:nodoc:
+ def parse_attributes!
self.attributes = (attributes || []).map do |attr|
Rails::Generators::GeneratedAttribute.parse(attr)
end
end
- def attributes_names
+ def attributes_names # :doc:
@attributes_names ||= attributes.each_with_object([]) do |a, names|
names << a.column_name
names << "password_confirmation" if a.password_digest?
@@ -199,11 +207,11 @@ module Rails
end
end
- def pluralize_table_names?
+ def pluralize_table_names? # :doc:
!defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names
end
- def mountable_engine?
+ def mountable_engine? # :doc:
defined?(ENGINE_ROOT) && namespaced?
end
@@ -217,9 +225,9 @@ module Rails
# If the generator is invoked with class name Admin, it will check for
# the presence of "AdminDecorator".
#
- def self.check_class_collision(options = {})
+ def self.check_class_collision(options = {}) # :doc:
define_method :check_class_collision do
- name = if self.respond_to?(:controller_class_name) # for ScaffoldBase
+ name = if respond_to?(:controller_class_name) # for ScaffoldBase
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 d6ffa2d89d..324843a5f5 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -32,6 +32,14 @@ module Rails
# This allows you to override entire operations, like the creation of the
# Gemfile, README, or JavaScript files, without needing to know exactly
# what those operations do so you can create another template action.
+ #
+ # class CustomAppBuilder < Rails::AppBuilder
+ # def test
+ # @generator.gem "rspec-rails", group: [:development, :test]
+ # run "bundle install"
+ # generate "rspec:install"
+ # end
+ # end
class AppBuilder
def rakefile
template "Rakefile"
@@ -53,6 +61,12 @@ module Rails
template "gitignore", ".gitignore"
end
+ def version_control
+ if !options[:skip_git] && !options[:pretend]
+ run "git init"
+ end
+ end
+
def app
directory "app"
@@ -70,6 +84,16 @@ module Rails
chmod "bin", 0755 & ~File.umask, verbose: false
end
+ def bin_when_updating
+ bin_yarn_exist = File.exist?("bin/yarn")
+
+ bin
+
+ if options[:api] && !bin_yarn_exist
+ remove_file "bin/yarn"
+ end
+ end
+
def config
empty_directory "config"
@@ -92,11 +116,11 @@ module Rails
cookie_serializer_config_exist = File.exist?("config/initializers/cookies_serializer.rb")
action_cable_config_exist = File.exist?("config/cable.yml")
rack_cors_config_exist = File.exist?("config/initializers/cors.rb")
+ assets_config_exist = File.exist?("config/initializers/assets.rb")
+ new_framework_defaults_5_1_exist = File.exist?("config/initializers/new_framework_defaults_5_1.rb")
config
- gsub_file "config/environments/development.rb", /^(\s+)config\.file_watcher/, '\1# config.file_watcher'
-
unless cookie_serializer_config_exist
gsub_file "config/initializers/cookies_serializer.rb", /json(?!,)/, "marshal"
end
@@ -108,6 +132,22 @@ module Rails
unless rack_cors_config_exist
remove_file "config/initializers/cors.rb"
end
+
+ if options[:api]
+ unless cookie_serializer_config_exist
+ remove_file "config/initializers/cookies_serializer.rb"
+ end
+
+ unless assets_config_exist
+ remove_file "config/initializers/assets.rb"
+ end
+
+ # Sprockets owns the only new default for 5.1:
+ # In API-only Applications, we don't want the file.
+ unless new_framework_defaults_5_1_exist
+ remove_file "config/initializers/new_framework_defaults_5_1.rb"
+ end
+ end
end
def database_yml
@@ -144,6 +184,12 @@ module Rails
template "test/test_helper.rb"
end
+ def system_test
+ empty_directory_with_keep_file "test/system"
+
+ template "test/application_system_test_case.rb"
+ end
+
def tmp
empty_directory_with_keep_file "tmp"
empty_directory "tmp/cache"
@@ -151,19 +197,12 @@ module Rails
end
def vendor
- vendor_javascripts
- vendor_stylesheets
- end
+ empty_directory_with_keep_file "vendor"
- def vendor_javascripts
- unless options[:skip_javascript]
- empty_directory_with_keep_file "vendor/assets/javascripts"
+ unless options[:skip_yarn]
+ template "package.json"
end
end
-
- def vendor_stylesheets
- empty_directory_with_keep_file "vendor/assets/stylesheets"
- end
end
module Generators
@@ -182,20 +221,21 @@ module Rails
class_option :api, type: :boolean,
desc: "Preconfigure smaller stack for API only apps"
+ class_option :skip_bundle, type: :boolean, aliases: "-B", default: false,
+ desc: "Don't run bundle install"
+
def initialize(*args)
super
- unless app_path
- raise Error, "Application name should be provided in arguments. For details run: rails --help"
- end
-
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
end
- # Force sprockets to be skipped when generating API only apps.
+ # Force sprockets and yarn to be skipped when generating API only apps.
# Can't modify options hash as it's frozen by default.
- self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze if options[:api]
+ if options[:api]
+ self.options = options.merge(skip_sprockets: true, skip_javascript: true, skip_yarn: true).freeze
+ end
end
public_task :set_default_accessors!
@@ -205,8 +245,9 @@ module Rails
build(:readme)
build(:rakefile)
build(:configru)
- build(:gitignore) unless options[:skip_git]
- build(:gemfile) unless options[:skip_gemfile]
+ build(:gitignore) unless options[:skip_git]
+ build(:gemfile) unless options[:skip_gemfile]
+ build(:version_control)
end
def create_app_files
@@ -217,6 +258,11 @@ module Rails
build(:bin)
end
+ def update_bin_files
+ build(:bin_when_updating)
+ end
+ remove_task :update_bin_files
+
def create_config_files
build(:config)
end
@@ -261,12 +307,20 @@ module Rails
build(:test) unless options[:skip_test]
end
+ def create_system_test_files
+ build(:system_test) if depends_on_system_test?
+ end
+
def create_tmp_files
build(:tmp)
end
def create_vendor_files
build(:vendor)
+
+ if options[:skip_yarn]
+ remove_file "package.json"
+ end
end
def delete_app_assets_if_api_option
@@ -274,7 +328,6 @@ module Rails
remove_dir "app/assets"
remove_dir "lib/assets"
remove_dir "tmp/cache/assets"
- remove_dir "vendor/assets"
end
end
@@ -322,7 +375,6 @@ module Rails
def delete_action_mailer_files_skipping_action_mailer
if options[:skip_action_mailer]
- remove_file "app/mailers/application_mailer.rb"
remove_file "app/views/layouts/mailer.html.erb"
remove_file "app/views/layouts/mailer.text.erb"
remove_dir "app/mailers"
@@ -350,23 +402,35 @@ module Rails
end
end
+ def delete_new_framework_defaults
+ # Sprockets owns the only new default for 5.1: if it's disabled,
+ # we don't want the file.
+ unless options[:update] && !options[:skip_sprockets]
+ remove_file "config/initializers/new_framework_defaults_5_1.rb"
+ end
+ end
+
+ def delete_bin_yarn_if_skip_yarn_option
+ remove_file "bin/yarn" if options[:skip_yarn]
+ end
+
def finish_template
build(:leftovers)
end
public_task :apply_rails_template, :run_bundle
- public_task :generate_spring_binstubs
+ public_task :run_webpack, :generate_spring_binstubs
def run_after_bundle_callbacks
@after_bundle_callbacks.each(&:call)
end
- protected
-
def self.banner
"rails new #{arguments.map(&:usage).join(' ')} [options]"
end
+ private
+
# Define file as an alias to create_file for backwards compatibility.
def file(*args, &block)
create_file(*args, &block)
@@ -423,7 +487,7 @@ module Rails
"/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4
"/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5
"/opt/lampp/var/mysql/mysql.sock" # xampp for linux
- ].find { |f| File.exist?(f) } unless RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
+ ].find { |f| File.exist?(f) } unless Gem.win_platform?
end
def get_builder_class
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index f1015b16d5..06f0dd6d6d 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -31,7 +31,12 @@ end
<% if RUBY_ENGINE == 'ruby' -%>
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
- gem 'byebug', platform: :mri
+ 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.0'
+ gem 'selenium-webdriver'
+ <%- end -%>
end
group :development do
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 5d633724d5..25870f19c8 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
@@ -1,8 +1,8 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
-// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
-// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
+// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
@@ -11,8 +11,10 @@
// about supported directives.
//
<% unless options[:skip_javascript] -%>
+<% if options[:javascript] -%>
//= require <%= options[:javascript] %>
-//= require <%= options[:javascript] %>_ujs
+<% end -%>
+//= require rails-ujs
<% unless options[:skip_turbolinks] -%>
//= require turbolinks
<% end -%>
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
index 0ebd7fe829..865300bef9 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
@@ -2,8 +2,8 @@
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
index 8635e97b76..c6607dbb2b 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
@@ -16,8 +16,12 @@ chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
-<% unless options.skip_active_record -%>
+<% unless options[:skip_yarn] %>
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+<% end %>
+<% unless options.skip_active_record -%>
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
# cp 'config/database.yml.sample', 'config/database.yml'
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 d385b363c6..d23af018c7 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt
+++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt
@@ -17,7 +17,6 @@ chdir APP_ROOT do
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
<% unless options.skip_active_record -%>
-
puts "\n== Updating database =="
system! 'bin/rails db:migrate'
<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn
new file mode 100644
index 0000000000..4ae896a8d3
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn
@@ -0,0 +1,9 @@
+VENDOR_PATH = File.expand_path('..', __dir__)
+Dir.chdir(VENDOR_PATH) do
+ begin
+ exec "yarnpkg #{ARGV.join(" ")}"
+ rescue Errno::ENOENT
+ puts "Yarn executable was not detected in the system."
+ puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ end
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb
index c0a0bd0a3e..d5d214052f 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb
@@ -22,6 +22,9 @@ 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 %>
+
# 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.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml b/railties/lib/rails/generators/rails/app/templates/config/cable.yml
index 0bbde6f74f..1da4913082 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml
@@ -7,3 +7,4 @@ test:
production:
adapter: redis
url: redis://localhost:6379/1
+ channel_prefix: <%= app_name %>_production
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
index a2b2a64ba6..8bc8735a8e 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
@@ -1,4 +1,4 @@
-# MySQL. Versions 5.0 and up are supported.
+# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver:
# gem install activerecord-jdbcmysql-adapter
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
index d987cf303b..269af1470d 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
@@ -1,4 +1,4 @@
-# MySQL. Versions 5.0 and up are supported.
+# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver
# gem install mysql2
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
index d2499ea4fb..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
@@ -1,4 +1,4 @@
-# Oracle/OCI 8i, 9, 10g
+# Oracle/OCI 11g or higher recommended
#
# Requires Ruby/OCI8:
# https://github.com/kubo/ruby-oci8
@@ -17,7 +17,7 @@
# cursor_sharing: similar
#
default: &default
- adapter: oracle
+ adapter: oracle_enhanced
pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= app_name %>
password:
@@ -45,7 +45,9 @@ test:
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#
-# DATABASE_URL="oracle://myuser:mypass@localhost/somedatabase"
+# DATABASE_URL="oracle-enhanced://myuser:mypass@localhost/somedatabase"
+#
+# Note that the adapter name uses a dash instead of an underscore.
#
# You can use this database configuration with:
#
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
index c223d6bc62..a21555e573 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
@@ -1,4 +1,4 @@
-# SQL Server (2005 or higher recommended)
+# SQL Server (2012 or higher recommended)
#
# Install the adapters and driver
# gem install tiny_tds
@@ -8,29 +8,12 @@
# gem 'tiny_tds'
# gem 'activerecord-sqlserver-adapter'
#
-# You should make sure freetds is configured correctly first.
-# freetds.conf contains host/port/protocol_versions settings.
-# http://freetds.schemamania.org/userguide/freetdsconf.htm
-#
-# A typical Microsoft server
-# [mssql]
-# host = mssqlserver.yourdomain.com
-# port = 1433
-# tds version = 7.1
-
-# If you can connect with "tsql -S servername", your basic FreeTDS installation is working.
-# 'man tsql' for more info
-# Set timeout to a larger number if valid queries against a live db fail
-#
default: &default
adapter: sqlserver
encoding: utf8
- pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
- reconnect: false
- username: <%= app_name %>
- password:
- timeout: 25
- dataserver: from_freetds.conf
+ username: sa
+ password: <%= ENV['SA_PASSWORD'] %>
+ host: localhost
development:
<<: *default
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 511b4a82eb..b75b65c8df 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
@@ -13,6 +13,7 @@ Rails.application.configure do
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
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 7deab5dbb1..9c4a77fd1d 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
@@ -14,6 +14,11 @@ Rails.application.configure do
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
+ # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
+ # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
+ # `config/secrets.yml.key`.
+ config.read_encrypted_secrets = true
+
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
@@ -88,7 +93,7 @@ Rails.application.configure do
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
- config.logger = ActiveSupport::TaggedLogging.new(logger)
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
end
<%- unless options.skip_active_record? -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
index 2318cf59ff..51196ae743 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
@@ -3,8 +3,12 @@
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
-# Add additional assets to the asset load path
+# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
+<%- unless options[:skip_yarn] -%>
+# Add Yarn node_modules folder to the asset load path.
+Rails.application.config.assets.paths << Rails.root.join('node_modules')
+<%- end -%>
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt
deleted file mode 100644
index 3ad3eba98a..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt
+++ /dev/null
@@ -1,40 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 5.0 upgrade.
-#
-<%- if options[:update] -%>
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-<%- end -%>
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-<%- unless options[:api] -%>
-
-# Enable per-form CSRF tokens. Previous versions had false.
-Rails.application.config.action_controller.per_form_csrf_tokens = <%= options[:update] ? false : true %>
-
-# Enable origin-checking CSRF mitigation. Previous versions had false.
-Rails.application.config.action_controller.forgery_protection_origin_check = <%= options[:update] ? false : true %>
-<%- end -%>
-
-# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
-# Previous versions had false.
-ActiveSupport.to_time_preserves_timezone = <%= options[:update] ? false : true %>
-<%- unless options[:skip_active_record] -%>
-
-# Require `belongs_to` associations by default. Previous versions had false.
-Rails.application.config.active_record.belongs_to_required_by_default = <%= options[:update] ? false : true %>
-<%- end -%>
-
-# Do not halt callback chains when a callback returns false. Previous versions had true.
-ActiveSupport.halt_callback_chains_on_return_false = <%= options[:update] ? true : false %>
-<%- unless options[:update] -%>
-
-# Configure SSL options to enable HSTS with subdomains. Previous versions had false.
-Rails.application.config.ssl_options = { hsts: { subdomains: true } }
-<%- end -%>
-<%- unless options[:skip_sprockets] -%>
-
-# Unknown asset fallback will return the path passed in when the given
-# asset is not present in the asset pipeline.
-Rails.application.config.assets.unknown_asset_fallback = <%= options[:update] ? true : false %>
-<%- end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt
new file mode 100644
index 0000000000..5f5545c4c7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt
@@ -0,0 +1,13 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 5.1 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.
+<%- unless options[:skip_sprockets] -%>
+
+# Unknown asset fallback will return the path passed in when the given
+# asset is not present in the asset pipeline.
+# Rails.application.config.assets.unknown_asset_fallback = false
+<%- end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml
index 0653957166..decc5a8573 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml
@@ -16,6 +16,16 @@
#
# This would use the information in config/locales/es.yml.
#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# 'true': 'foo'
+#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
index 7ee948002e..1e19380dcb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
@@ -1,13 +1,13 @@
# Puma can serve each request in a thread from an internal thread pool.
-# The `threads` method setting takes two numbers a minimum and maximum.
+# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
-# and maximum, this matches the default thread size of Active Record.
+# and maximum; this matches the default thread size of Active Record.
#
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
-# Specifies the `port` that Puma will listen on to receive requests, default is 3000.
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
@@ -42,9 +42,9 @@ environment ENV.fetch("RAILS_ENV") { "development" }
# 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 `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, Ruby
+# 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
diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
index 8e995a5df1..ea9d47396c 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
@@ -12,8 +12,8 @@
# Shared secrets are available across all environments.
-shared:
- api_key: 123
+# shared:
+# api_key: a1B2c3D4e5F6
# Environmental secrets are only available for that specific environment.
@@ -23,8 +23,10 @@ development:
test:
secret_key_base: <%= app_secret %>
-# Do not keep production secrets in the repository,
-# instead read values from the environment.
+# 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/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore
index 0e66cc4237..7221c26729 100644
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ b/railties/lib/rails/generators/rails/app/templates/gitignore
@@ -21,5 +21,9 @@
!/tmp/.keep
<% end -%>
-# Ignore Byebug command history file.
+<% unless options[:skip_yarn] -%>
+/node_modules
+/yarn-error.log
+
+<% end -%>
.byebug_history
diff --git a/railties/lib/rails/generators/rails/app/templates/package.json b/railties/lib/rails/generators/rails/app/templates/package.json
new file mode 100644
index 0000000000..46db57dcbe
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "<%= app_name %>",
+ "private": true,
+ "dependencies": {}
+}
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
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb
@@ -0,0 +1,5 @@
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+end
diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb
index 265dada2ca..95d00c2d39 100644
--- a/railties/lib/rails/generators/rails/assets/assets_generator.rb
+++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb
@@ -7,7 +7,7 @@ module Rails
class_option :javascript_engine, desc: "Engine for JavaScripts"
class_option :stylesheet_engine, desc: "Engine for Stylesheets"
- protected
+ private
def asset_name
file_name
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index ced3c85c00..06bdb8b5ce 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -16,7 +16,7 @@ module Rails
unless options[:skip_routes]
actions.reverse_each do |action|
# route prepends two spaces onto the front of the string that is passed, this corrects that.
- route generate_routing_code(action)[2..-1]
+ route indent(generate_routing_code(action), 2)[2..-1]
end
end
end
@@ -34,27 +34,30 @@ module Rails
# end
# end
def generate_routing_code(action)
- depth = regular_class_path.length
+ depth = 0
+ lines = []
+
# Create 'namespace' ladder
# namespace :foo do
# namespace :bar do
- namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
- indent(" namespace :#{ns} do\n", i * 2)
- end.join
+ regular_class_path.each do |ns|
+ lines << indent("namespace :#{ns} do\n", depth * 2)
+ depth += 1
+ end
# Create route
# get 'baz/index'
- route = indent(%{ get '#{file_name}/#{action}'\n}, depth * 2)
+ lines << indent(%{get '#{file_name}/#{action}'\n}, depth * 2)
# Create `end` ladder
# end
# end
- end_ladder = (1..depth).reverse_each.map do |i|
- indent("end\n", i * 2)
- end.join
+ until depth.zero?
+ depth -= 1
+ lines << indent("end\n", depth * 2)
+ end
- # Combine the 3 parts to generate complete route entry
- namespace_ladder + route + end_ladder
+ lines.join
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
new file mode 100644
index 0000000000..8b29213610
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb
@@ -0,0 +1,66 @@
+require "rails/generators/base"
+require "rails/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 File.exist?("config/secrets.yml.enc")
+ say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted."
+ say ""
+
+ template "config/secrets.yml.enc" do |prefill|
+ say ""
+ say "For now the file contains this but it's been encrypted with the generated key:"
+ say ""
+ say prefill, :on_green
+ say ""
+
+ Secrets.encrypt(prefill)
+ end
+
+ say "You can edit encrypted secrets with `bin/rails secrets:edit`."
+
+ say "Add this to your config/environments/production.rb:"
+ say "config.read_encrypted_secrets = true"
+ end
+ 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/encrypted_secrets/templates/config/secrets.yml.enc b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc
new file mode 100644
index 0000000000..70426a66a5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc
@@ -0,0 +1,3 @@
+# See `secrets.yml` for tips on generating suitable keys.
+# production:
+# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289…
diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb
index 8040ec5e7b..299a7da5f1 100644
--- a/railties/lib/rails/generators/rails/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb
@@ -12,7 +12,7 @@ module Rails
hook_for :test_framework
- protected
+ private
def generator_dir
if options[:namespace]
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 2186fa4ded..118e44d9d0 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -91,6 +91,8 @@ task default: :test
opts[:skip_bundle] = true
opts[:api] = options.api?
opts[:skip_listen] = true
+ opts[:skip_git] = true
+ opts[:skip_turbolinks] = true
invoke Rails::Generators::AppGenerator,
[ File.expand_path(dummy_path, destination_root) ], opts
@@ -112,7 +114,6 @@ task default: :test
def test_dummy_clean
inside dummy_path do
- remove_file ".gitignore"
remove_file "db/seeds.rb"
remove_file "doc"
remove_file "Gemfile"
@@ -186,7 +187,7 @@ task default: :test
desc: "Skip gemspec file"
class_option :skip_gemfile_entry, type: :boolean, default: false,
- desc: "If creating plugin in application's directory " +
+ desc: "If creating plugin in application's directory " \
"skip adding entry to Gemfile"
class_option :api, type: :boolean, default: false,
@@ -195,10 +196,6 @@ task default: :test
def initialize(*args)
@dummy_path = nil
super
-
- unless plugin_path
- raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help"
- end
end
public_task :set_default_accessors!
@@ -286,7 +283,7 @@ task default: :test
@namespaced_name ||= name.tr("-", "/")
end
- protected
+ private
def create_dummy_app(path = nil)
dummy_path(path) if path
@@ -304,7 +301,7 @@ task default: :test
end
def engine?
- full? || mountable?
+ full? || mountable? || options[:engine]
end
def full?
@@ -415,7 +412,6 @@ task default: :test
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
- t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
@@ -437,7 +433,7 @@ end
end
def inside_application?
- rails_app_path && app_path =~ /^#{rails_app_path}/
+ rails_app_path && destination_root.start_with?(rails_app_path.to_s)
end
def relative_path
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 56e7925c6b..c03d9953d4 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -3,6 +3,7 @@
ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__)
+APP_PATH = File.expand_path('../../<%= dummy_path -%>/config/application', __FILE__)
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
index c0fbb84a93..8385e6a8a2 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
@@ -1,10 +1,4 @@
-$: << File.expand_path(File.expand_path('../../test', __FILE__))
+$: << File.expand_path(File.expand_path("../../test", __FILE__))
-require 'bundler/setup'
-require 'rails/test_unit/minitest_plugin'
-
-Rails::TestUnitReporter.executable = 'bin/test'
-
-Minitest.run_via[:rails] = true
-
-require "active_support/testing/autorun"
+require "bundler/setup"
+require "rails/plugin/test"
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
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb
@@ -0,0 +1,5 @@
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+end
diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
index ed6bf7f7d7..12d6bc85b2 100644
--- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
@@ -6,6 +6,7 @@ module Rails
remove_hook_for :resource_controller
remove_class_option :actions
+ class_option :api, type: :boolean
class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets"
class_option :stylesheet_engine, desc: "Engine for Stylesheets"
class_option :assets, type: :boolean
@@ -15,10 +16,13 @@ module Rails
def handle_skip
@options = @options.merge(stylesheets: false) unless options[:assets]
@options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] && options[:scaffold_stylesheet]
+ @options = @options.merge(system_tests: false) if options[:api]
end
hook_for :scaffold_controller, required: true
+ hook_for :system_tests, as: :system
+
hook_for :assets do |assets|
invoke assets, [controller_name]
end
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 e4f3161ffd..cf97c22160 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
@@ -20,7 +20,11 @@ module Rails
template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
end
- hook_for :template_engine, :test_framework, as: :scaffold
+ hook_for :template_engine, as: :scaffold do |template_engine|
+ invoke template_engine unless options.api?
+ end
+
+ hook_for :test_framework, as: :scaffold
# Invoke the helper using the controller name (pluralized)
hook_for :helper, as: :scaffold do |invoked|
diff --git a/railties/lib/rails/generators/rails/system_test/USAGE b/railties/lib/rails/generators/rails/system_test/USAGE
new file mode 100644
index 0000000000..f11a99e008
--- /dev/null
+++ b/railties/lib/rails/generators/rails/system_test/USAGE
@@ -0,0 +1,10 @@
+Description:
+ Stubs out a new system test. Pass the name of the test, either
+ CamelCased or under_scored, as an argument.
+
+ This generator invokes the current system tool, which defaults to
+ TestUnit.
+
+Example:
+ `rails generate system_test GeneralStories` creates a GeneralStories
+ system test in test/system/general_stories_test.rb
diff --git a/railties/lib/rails/generators/rails/system_test/system_test_generator.rb b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb
new file mode 100644
index 0000000000..901120e892
--- /dev/null
+++ b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb
@@ -0,0 +1,7 @@
+module Rails
+ module Generators
+ class SystemTestGenerator < NamedBase # :nodoc:
+ hook_for :system_tests, as: :system
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index a28977319a..e7cb722473 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -23,10 +23,14 @@ 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
+
def controller_class_path
if options[:model_name]
@controller_class_path
@@ -55,7 +59,7 @@ module Rails
end
# Loads the ORM::Generators::ActiveModel class. This class is responsible
- # to tell scaffold entities how to generate an specific method for the
+ # to tell scaffold entities how to generate a specific method for the
# ORM. Check Rails::Generators::ActiveModel for more information.
def orm_class
@orm_class ||= begin
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 59f8d40343..6b6e094453 100644
--- a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
@@ -12,7 +12,7 @@ module TestUnit # :nodoc:
template "generator_test.rb", File.join("test/lib/generators", class_path, "#{file_name}_generator_test.rb")
end
- protected
+ private
def generator_path
if options[:namespace]
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
index dea7e22196..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
@@ -1,7 +1,9 @@
require 'test_helper'
+<% module_namespacing do -%>
class <%= class_name %>Test < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end
+<% end -%>
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 806279788e..67bff8e4f9 100644
--- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
@@ -17,7 +17,7 @@ module TestUnit # :nodoc:
template "preview.rb", File.join("test/mailers/previews", class_path, "#{file_name}_mailer_preview.rb")
end
- protected
+ private
def file_name
@_file_name ||= super.gsub(/_mailer/i, "")
end
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 8840a86d0d..292db35121 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
@@ -22,7 +22,7 @@ module TestUnit # :nodoc:
def fixture_name
@fixture_name ||=
if mountable_engine?
- "%s_%s" % [namespaced_path, table_name]
+ (namespace_dirs + [table_name]).join("_")
else
table_name
end
diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb
new file mode 100644
index 0000000000..aec415a4e5
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb
@@ -0,0 +1,17 @@
+require "rails/generators/test_unit"
+
+module TestUnit # :nodoc:
+ module Generators # :nodoc:
+ class SystemGenerator < Base # :nodoc:
+ check_class_collision suffix: "Test"
+
+ def create_test_files
+ if !File.exist?(File.join("test/application_system_test_case.rb"))
+ template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb")
+ end
+
+ template "system_test.rb", File.join("test/system", "#{file_name.pluralize}_test.rb")
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..d19212abd5
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb
@@ -0,0 +1,5 @@
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+end
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
new file mode 100644
index 0000000000..b5ce2ba5c8
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb
@@ -0,0 +1,9 @@
+require "application_system_test_case"
+
+class <%= class_name.pluralize %>Test < ApplicationSystemTestCase
+ # test "visiting the index" do
+ # visit <%= plural_table_name %>_url
+ #
+ # assert_selector "h1", text: "<%= class_name %>"
+ # end
+end
diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb
index a1e5a233b9..64d641d096 100644
--- a/railties/lib/rails/generators/testing/behaviour.rb
+++ b/railties/lib/rails/generators/testing/behaviour.rb
@@ -82,23 +82,23 @@ module Rails
Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(":"))
end
- protected
+ private
- def destination_root_is_set? # :nodoc:
+ def destination_root_is_set?
raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root
end
- def ensure_current_path # :nodoc:
+ def ensure_current_path
cd current_path
end
# Clears all files and directories in destination.
- def prepare_destination
+ def prepare_destination # :doc:
rm_rf(destination_root)
mkdir_p(destination_root)
end
- def migration_file_name(relative) # :nodoc:
+ def migration_file_name(relative)
absolute = File.expand_path(relative, destination_root)
dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, "")
Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb
index 5d4acd6f6b..fc064dac32 100644
--- a/railties/lib/rails/info.rb
+++ b/railties/lib/rails/info.rb
@@ -1,9 +1,9 @@
require "cgi"
module Rails
- # This module helps build the runtime properties used to display in the
- # Rails::InfoController responses. Including the active Rails version, Ruby
- # version, Rack version, and so on.
+ # This module helps build the runtime properties that are displayed in
+ # Rails::InfoController responses. These include the active Rails version,
+ # Ruby version, Rack version, and so on.
module Info
mattr_accessor :properties
class << (@@properties = [])
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index 95de998208..000ce40fbc 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -40,12 +40,12 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
end
- protected
- def show_previews?
+ private
+ def show_previews? # :doc:
ActionMailer::Base.show_previews
end
- def find_preview
+ def find_preview # :doc:
candidates = []
params[:path].to_s.scan(%r{/|$}) { candidates << $` }
preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) }
@@ -57,7 +57,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
end
- def find_preferred_part(*formats)
+ def find_preferred_part(*formats) # :doc:
formats.each do |format|
if part = @email.find_first_mime_type(format)
return part
@@ -69,7 +69,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
end
- def find_part(format)
+ def find_part(format) # :doc:
if part = @email.find_first_mime_type(format)
part
elsif @email.mime_type == format
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index 1c1810dde6..af3be10a31 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -2,12 +2,12 @@ module Rails
module Paths
# This object is an extended hash that behaves as root of the <tt>Rails::Paths</tt> system.
# It allows you to collect information about how you want to structure your application
- # paths by a Hash like API. It requires you to give a physical path on initialization.
+ # paths through a Hash-like API. It requires you to give a physical path on initialization.
#
# root = Root.new "/rails"
# root.add "app/controllers", eager_load: true
#
- # The command above creates a new root object and adds "app/controllers" as a path.
+ # The above command creates a new root object and adds "app/controllers" as a path.
# This means we can get a <tt>Rails::Paths::Path</tt> object back like below:
#
# path = root["app/controllers"]
@@ -45,7 +45,6 @@ module Rails
attr_accessor :path
def initialize(path)
- @current = nil
@path = path
@root = {}
end
diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb
new file mode 100644
index 0000000000..ff043b488e
--- /dev/null
+++ b/railties/lib/rails/plugin/test.rb
@@ -0,0 +1,7 @@
+require "rails/test_unit/minitest_plugin"
+
+Rails::TestUnitReporter.executable = "bin/test"
+
+Minitest.run_via = :rails
+
+require "active_support/testing/autorun"
diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb
deleted file mode 100644
index dccfa3e9bb..0000000000
--- a/railties/lib/rails/rack/debugger.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-require "active_support/deprecation"
-
-ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.")
diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb
index e3fee75603..853fc26051 100644
--- a/railties/lib/rails/rack/logger.rb
+++ b/railties/lib/rails/rack/logger.rb
@@ -27,45 +27,43 @@ module Rails
end
end
- protected
+ private
- def call_app(request, env)
- 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
- rescue Exception
- finish(request)
- raise
- ensure
- ActiveSupport::LogSubscriber.flush_all!
- end
+ def call_app(request, env) # :doc:
+ 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
+ rescue Exception
+ finish(request)
+ raise
+ ensure
+ ActiveSupport::LogSubscriber.flush_all!
+ end
- # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700
- def started_request_message(request)
- 'Started %s "%s" for %s at %s' % [
- request.request_method,
- request.filtered_path,
- request.ip,
- Time.now.to_default_s ]
- end
+ # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700
+ def started_request_message(request) # :doc:
+ 'Started %s "%s" for %s at %s' % [
+ request.request_method,
+ request.filtered_path,
+ request.ip,
+ Time.now.to_default_s ]
+ end
- def compute_tags(request)
- @taggers.collect do |tag|
- case tag
- when Proc
- tag.call(request)
- when Symbol
- request.send(tag)
- else
- tag
+ def compute_tags(request) # :doc:
+ @taggers.collect do |tag|
+ case tag
+ when Proc
+ tag.call(request)
+ when Symbol
+ request.send(tag)
+ else
+ tag
+ end
end
end
- end
-
- private
def finish(request)
instrumenter = ActiveSupport::Notifications.instrumenter
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 696db61f01..474118c0a9 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -173,8 +173,8 @@ module Rails
instance.configure(&block)
end
- protected
- def generate_railtie_name(string) #:nodoc:
+ private
+ def generate_railtie_name(string)
ActiveSupport::Inflector.underscore(string).tr("/", "_")
end
@@ -188,7 +188,6 @@ module Rails
end
end
- private
# receives an instance variable identifier, set the variable value if is
# blank and append given block to value, which will be used later in
# `#each_registered_block(type, &block)`
diff --git a/railties/lib/rails/railtie/configurable.rb b/railties/lib/rails/railtie/configurable.rb
index 39f1f87575..2a8295426e 100644
--- a/railties/lib/rails/railtie/configurable.rb
+++ b/railties/lib/rails/railtie/configurable.rb
@@ -24,7 +24,7 @@ module Rails
class_eval(&block)
end
- protected
+ private
def method_missing(*args, &block)
instance.send(*args, &block)
diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb
new file mode 100644
index 0000000000..2a95712cd9
--- /dev/null
+++ b/railties/lib/rails/secrets.rb
@@ -0,0 +1,106 @@
+require "yaml"
+require "active_support/message_encryptor"
+
+module Rails
+ # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘
+ class Secrets # :nodoc:
+ class MissingKeyError < RuntimeError
+ def initialize
+ super(<<-end_of_message.squish)
+ Missing encryption key to decrypt secrets with.
+ Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"]
+ end_of_message
+ end
+ end
+
+ @cipher = "aes-128-gcm"
+ @read_encrypted_secrets = false
+ @root = File # Wonky, but ensures `join` uses the current directory.
+
+ class << self
+ attr_writer :root
+ attr_accessor :read_encrypted_secrets
+
+ def parse(paths, env:)
+ paths.each_with_object(Hash.new) do |path, all_secrets|
+ require "erb"
+
+ secrets = YAML.load(ERB.new(preprocess(path)).result) || {}
+ all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
+ all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env]
+ 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 encrypt(data)
+ encryptor.encrypt_and_sign(data)
+ end
+
+ def decrypt(data)
+ encryptor.decrypt_and_verify(data)
+ end
+
+ def read
+ decrypt(IO.binread(path))
+ end
+
+ def write(contents)
+ IO.binwrite("#{path}.tmp", encrypt(contents))
+ FileUtils.mv("#{path}.tmp", path)
+ end
+
+ def read_for_editing
+ tmp_path = File.join(Dir.tmpdir, File.basename(path))
+ IO.binwrite(tmp_path, read)
+
+ yield tmp_path
+
+ write(IO.binread(tmp_path))
+ ensure
+ FileUtils.rm(tmp_path) if File.exist?(tmp_path)
+ end
+
+ private
+ def handle_missing_key
+ raise MissingKeyError
+ end
+
+ def read_key_file
+ if File.exist?(key_path)
+ IO.binread(key_path).strip
+ end
+ end
+
+ def key_path
+ @root.join("config", "secrets.yml.key")
+ end
+
+ def path
+ @root.join("config", "secrets.yml.enc").to_s
+ end
+
+ def preprocess(path)
+ if path.end_with?(".enc")
+ if @read_encrypted_secrets
+ decrypt(IO.binread(path))
+ else
+ ""
+ end
+ else
+ IO.read(path)
+ end
+ end
+
+ def encryptor
+ @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 967e969f81..e9088c44ce 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -13,7 +13,7 @@
# 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)
+ Annotation = Struct.new(:line, :tag, :text) do
def self.directories
@@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",")
end
@@ -116,7 +116,7 @@ class SourceAnnotationExtractor
# Otherwise it returns an empty hash.
def extract_annotations_from(file, pattern)
lineno = 0
- result = File.readlines(file).inject([]) do |list, line|
+ result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line|
lineno += 1
next list unless line =~ pattern
list << Annotation.new(lineno, $1, $2)
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 48675b3845..c0a50e5bda 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -12,6 +12,7 @@ require "rake"
restart
routes
tmp
+ yarn
).tap { |arr|
arr << "statistics" if Rake.application.current_scope.empty?
}.each do |task|
diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake
index c92b42f6c1..177b138090 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -1,6 +1,17 @@
task "load_app" do
namespace :app do
load APP_RAKEFILE
+
+ desc "Update some initially generated files"
+ task update: [ "update:bin" ]
+
+ namespace :update do
+ require "rails/engine/updater"
+ # desc "Adds new executables to the engine bin/ directory"
+ task :bin do
+ Rails::Engine::Updater.run(:create_bin_files)
+ end
+ end
end
task environment: "app:environment"
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index a2167796eb..32a6b109bc 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,5 +1,3 @@
-require "active_support/deprecation"
-
namespace :app do
desc "Update configs and some other initially generated files (or use just update:configs or update:bin)"
task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ]
@@ -65,7 +63,7 @@ namespace :app do
# desc "Adds new executables to the application bin/ directory"
task :bin do
- RailsUpdate.invoke_from_app_generator :create_bin_files
+ RailsUpdate.invoke_from_app_generator :update_bin_files
end
task :upgrade_guide_info do
@@ -73,15 +71,3 @@ namespace :app do
end
end
end
-
-namespace :rails do
- %i(update template templates:copy update:configs update:bin).each do |task_name|
- task "#{task_name}" do
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace.
- Run bin/rails app:#{task_name} instead.
- MSG
- Rake.application.invoke_task("app:#{task_name}")
- end
- end
-end
diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake
index c376234fee..ba796845d7 100644
--- a/railties/lib/rails/tasks/log.rake
+++ b/railties/lib/rails/tasks/log.rake
@@ -3,7 +3,7 @@ namespace :log do
##
# Truncates all/specified log files
# ENV['LOGS']
- # - defaults to standard environment log files i.e. 'development,test,production'
+ # - defaults to all environments log files i.e. 'development,test,production'
# - ENV['LOGS']=all truncates all files i.e. log/*.log
# - ENV['LOGS']='test,development' truncates only specified files
desc "Truncates all/specified *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)"
@@ -19,7 +19,7 @@ namespace :log do
elsif ENV["LOGS"]
log_files_to_truncate(ENV["LOGS"])
else
- log_files_to_truncate("development,test,production")
+ log_files_to_truncate(all_environments.join(","))
end
end
@@ -33,4 +33,8 @@ namespace :log do
f = File.open(file, "w")
f.close
end
+
+ def all_environments
+ Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") }
+ end
end
diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake
index f5e5b9ae87..215fb2ceb5 100644
--- a/railties/lib/rails/tasks/routes.rake
+++ b/railties/lib/rails/tasks/routes.rake
@@ -1,5 +1,3 @@
-require "active_support/deprecation"
-require "active_support/core_ext/string/strip" # for strip_heredoc
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"
@@ -7,15 +5,8 @@ task routes: :environment do
all_routes = Rails.application.routes.routes
require "action_dispatch/routing/inspector"
inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
- if ARGV.any? { |argv| argv.start_with? "CONTROLLER" }
- puts <<-eow.strip_heredoc
- Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.
- Please use `bin/rails routes -c controller_name` instead.
- eow
- end
routes_filter = nil
- routes_filter = { controller: ENV["CONTROLLER"] } if ENV["CONTROLLER"]
OptionParser.new do |opts|
opts.banner = "Usage: rails routes [options]"
diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake
index a6cdd1e99c..cb569be58b 100644
--- a/railties/lib/rails/tasks/statistics.rake
+++ b/railties/lib/rails/tasks/statistics.rake
@@ -8,9 +8,8 @@ STATS_DIRECTORIES = [
%w(Models app/models),
%w(Mailers app/mailers),
%w(Channels app/channels),
- %w(Javascripts app/assets/javascripts),
+ %w(JavaScripts app/assets/javascripts),
%w(Libraries lib/),
- %w(Tasks lib/tasks),
%w(APIs app/apis),
%w(Controller\ tests test/controllers),
%w(Helper\ tests test/helpers),
@@ -18,6 +17,7 @@ STATS_DIRECTORIES = [
%w(Mailer\ tests test/mailers),
%w(Job\ tests test/jobs),
%w(Integration\ tests test/integration),
+ %w(System\ tests test/system),
].collect do |name, dir|
[ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ]
end.select { |name, dir| File.directory?(dir) }
diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake
new file mode 100644
index 0000000000..87476b1b8c
--- /dev/null
+++ b/railties/lib/rails/tasks/yarn.rake
@@ -0,0 +1,11 @@
+namespace :yarn do
+ desc "Install all JavaScript dependencies as specified via Yarn"
+ task :install do
+ system("./bin/yarn install --no-progress")
+ end
+end
+
+# Run Yarn prior to Sprockets assets precompilation, so dependencies are available for use.
+if Rake::Task.task_defined?("assets:precompile")
+ Rake::Task["assets:precompile"].enhance [ "yarn:install" ]
+end
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index db341dd847..0f9bf98737 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -14,10 +14,12 @@ require "active_support/testing/autorun"
if defined?(ActiveRecord::Base)
ActiveRecord::Migration.maintain_test_schema!
- class ActiveSupport::TestCase
- include ActiveRecord::TestFixtures
- self.fixture_path = "#{Rails.root}/test/fixtures/"
- self.file_fixture_path = self.fixture_path + "files"
+ module ActiveSupport
+ class TestCase
+ include ActiveRecord::TestFixtures
+ self.fixture_path = "#{Rails.root}/test/fixtures/"
+ self.file_fixture_path = fixture_path + "files"
+ end
end
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
@@ -27,6 +29,8 @@ if defined?(ActiveRecord::Base)
end
end
+# :enddoc:
+
class ActionController::TestCase
def before_setup # :nodoc:
@routes = Rails.application.routes
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index 6e196a32ab..8decdb0f4f 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -52,21 +52,25 @@ module Minitest
options[:color] = value
end
+ opts.on("-w", "--warnings",
+ "Enable ruby warnings") do
+ $VERBOSE = true
+ end
+
options[:color] = true
options[:output_inline] = true
- options[:patterns] = defined?(@rake_patterns) ? @rake_patterns : opts.order!
+ options[:patterns] = opts.order! unless run_via.rake?
end
- # Running several Rake tasks in a single command would trip up the runner,
- # as the patterns would also contain the other Rake tasks.
- def self.rake_run(patterns) # :nodoc:
- @rake_patterns = patterns
+ def self.rake_run(patterns, exclude_patterns = []) # :nodoc:
+ self.run_via = :rake unless run_via.set?
+ ::Rails::TestRequirer.require_files(patterns, exclude_patterns)
autorun
end
module RunRespectingRakeTestopts
def run(args = [])
- if defined?(@rake_patterns)
+ if run_via.rake?
args = Shellwords.split(ENV["TESTOPTS"] || "")
end
@@ -81,9 +85,16 @@ module Minitest
def self.plugin_rails_init(options)
ENV["RAILS_ENV"] = options[:environment] || "test"
- # If run via `ruby` we've been passed the files to run directly.
- unless run_via[:ruby]
- ::Rails::TestRequirer.require_files(options[:patterns])
+ # If run via `ruby` we've been passed the files to run directly, or if run
+ # via `rake` then they have already been eagerly required.
+ unless run_via.ruby? || run_via.rake?
+ # If there are no given patterns, we can assume that the user
+ # simply runs the `bin/rails test` command without extra arguments.
+ if options[:patterns].empty?
+ ::Rails::TestRequirer.require_files(options[:patterns], ["test/system/**/*"])
+ else
+ ::Rails::TestRequirer.require_files(options[:patterns])
+ end
end
unless options[:full_backtrace] || ENV["BACKTRACE"]
@@ -97,7 +108,33 @@ module Minitest
reporter << ::Rails::TestUnitReporter.new(options[:io], options)
end
- mattr_accessor(:run_via) { Hash.new }
+ def self.run_via=(runner)
+ if run_via.set?
+ raise ArgumentError, "run_via already assigned"
+ else
+ run_via.runner = runner
+ end
+ end
+
+ class RunVia
+ attr_accessor :runner
+ alias set? runner
+
+ # Backwardscompatibility with Rails 5.0 generated plugin test scripts.
+ def []=(runner, *)
+ @runner = runner
+ end
+
+ def ruby?
+ runner == :ruby
+ end
+
+ def rake?
+ runner == :rake
+ end
+ end
+
+ mattr_reader(:run_via) { RunVia.new }
end
# Put Rails as the first plugin minitest initializes so other plugins
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index 746120e6a1..9cc3f73a9c 100644
--- a/railties/lib/rails/test_unit/railtie.rb
+++ b/railties/lib/rails/test_unit/railtie.rb
@@ -11,6 +11,7 @@ module Rails
fixture_replacement: nil
c.integration_tool :test_unit
+ c.system_tests :test_unit
end
initializer "test_unit.line_filtering" do
diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb
index fe35934abc..92e5fcf0bc 100644
--- a/railties/lib/rails/test_unit/test_requirer.rb
+++ b/railties/lib/rails/test_unit/test_requirer.rb
@@ -4,10 +4,13 @@ require "rake/file_list"
module Rails
class TestRequirer # :nodoc:
class << self
- def require_files(patterns)
+ def require_files(patterns, exclude_patterns = [])
patterns = expand_patterns(patterns)
- Rake::FileList[patterns.compact.presence || "test/**/*_test.rb"].to_a.each do |file|
+ file_list = Rake::FileList[patterns.compact.presence || "test/**/*_test.rb"]
+ file_list.exclude(exclude_patterns)
+
+ file_list.to_a.each do |file|
require File.expand_path(file)
end
end
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index 4c157c1262..ef19bd7626 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -4,15 +4,15 @@ require "rails/test_unit/minitest_plugin"
task default: :test
-desc "Runs all tests in test folder"
+desc "Runs all tests in test folder except system ones"
task :test do
$: << "test"
- pattern = if ENV.key?("TEST")
- ENV["TEST"]
+
+ if ENV.key?("TEST")
+ Minitest.rake_run([ENV["TEST"]])
else
- "test"
+ Minitest.rake_run(["test"], ["test/system/**/*"])
end
- Minitest.rake_run([pattern])
end
namespace :test do
@@ -47,4 +47,9 @@ namespace :test do
$: << "test"
Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"])
end
+
+ task system: "test:prepare" do
+ $: << "test"
+ Minitest.rake_run(["test/system"])
+ end
end
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index fd1e1b9662..e4b2d0457d 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -17,15 +17,15 @@ module TestApp
end
end
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
-end
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
-end
-
class ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
+
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ private def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ private def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index edd43503bf..f38cacd6da 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -384,7 +384,7 @@ module ApplicationTests
get "/assets/demo.js"
assert_match "alert()", last_response.body
- assert_equal nil, last_response.headers["Set-Cookie"]
+ assert_nil last_response.headers["Set-Cookie"]
end
test "files in any assets/ directories are not added to Sprockets" do
diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb
index 0bbd25db2b..0fb995900f 100644
--- a/railties/test/application/bin_setup_test.rb
+++ b/railties/test/application/bin_setup_test.rb
@@ -6,17 +6,10 @@ module ApplicationTests
def setup
build_app
-
- create_gemfile
- update_boot_file_to_use_bundler
- @old_gemfile_env = ENV["BUNDLE_GEMFILE"]
- ENV["BUNDLE_GEMFILE"] = app_path + "/Gemfile"
end
def teardown
teardown_app
-
- ENV["BUNDLE_GEMFILE"] = @old_gemfile_env
end
def test_bin_setup
@@ -45,6 +38,10 @@ module ApplicationTests
app_file "db/schema.rb", ""
output = `bin/setup 2>&1`
+
+ # Ignore line that's only output by Bundler < 1.14
+ output.sub!(/^Resolving dependencies\.\.\.\n/, "")
+
assert_equal(<<-OUTPUT, output)
== Installing dependencies ==
The Gemfile's dependencies are satisfied
@@ -59,16 +56,5 @@ Created database 'db/test.sqlite3'
OUTPUT
end
end
-
- private
- def create_gemfile
- app_file("Gemfile", "source 'https://rubygems.org'")
- app_file("Gemfile", "gem 'rails', path: '#{RAILS_FRAMEWORK_ROOT}'", "a")
- app_file("Gemfile", "gem 'sqlite3'", "a")
- end
-
- def update_boot_file_to_use_bundler
- app_file("config/boot.rb", "require 'bundler/setup'")
- end
end
end
diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb
index 13fc98f0d6..8360b7bf4b 100644
--- a/railties/test/application/configuration/custom_test.rb
+++ b/railties/test/application/configuration/custom_test.rb
@@ -10,7 +10,6 @@ module ApplicationTests
def teardown
teardown_app
- FileUtils.rm_rf(new_app) if File.directory?(new_app)
end
test "access custom configuration point" do
@@ -28,30 +27,16 @@ module ApplicationTests
assert_equal 3, x.payment_processing.retries
assert_equal true, x.super_debugger
assert_equal false, x.hyper_debugger
- assert_equal nil, x.nil_debugger
+ assert_nil x.nil_debugger
assert_nil x.i_do_not_exist.zomg
- end
- test "custom configuration responds to all messages" do
- x = Rails.configuration.x
+ # test that custom configuration responds to all messages
assert_equal true, x.respond_to?(: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
private
- def new_app
- File.expand_path("#{app_path}/../new_app")
- end
-
- def copy_app
- FileUtils.cp_r(app_path, new_app)
- end
-
- def app
- @app ||= Rails.application
- end
-
def require_environment
require "#{app_path}/config/environment"
end
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index be84cd5027..14433fbba0 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -51,7 +51,7 @@ module ApplicationTests
def setup
build_app
- supress_default_config
+ suppress_default_config
end
def teardown
@@ -59,7 +59,7 @@ module ApplicationTests
FileUtils.rm_rf(new_app) if File.directory?(new_app)
end
- def supress_default_config
+ def suppress_default_config
FileUtils.mv("#{app_path}/config/environments", "#{app_path}/config/__environments__")
end
@@ -78,6 +78,18 @@ module ApplicationTests
end
end
+ test "Rails.env falls back to development if RAILS_ENV is blank and RACK_ENV is nil" do
+ with_rails_env("") do
+ assert_equal "development", Rails.env
+ end
+ end
+
+ test "Rails.env falls back to development if RACK_ENV is blank and RAILS_ENV is nil" do
+ with_rack_env("") do
+ assert_equal "development", Rails.env
+ end
+ end
+
test "By default logs tags are not set in development" do
restore_default_config
@@ -369,26 +381,6 @@ module ApplicationTests
end
end
- test "config.serve_static_files is deprecated" do
- make_basic_app do |application|
- assert_deprecated do
- application.config.serve_static_files = true
- end
-
- assert application.config.public_file_server.enabled
- end
- end
-
- test "config.static_cache_control is deprecated" do
- make_basic_app do |application|
- assert_deprecated do
- application.config.static_cache_control = "public, max-age=60"
- end
-
- assert_equal application.config.static_cache_control, "public, max-age=60"
- end
- end
-
test "Use key_generator when secret_key_base is set" do
make_basic_app do |application|
application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
@@ -614,7 +606,7 @@ module ApplicationTests
app "development"
assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token
- assert_equal nil, app.secrets.secret_key_base
+ assert_nil app.secrets.secret_key_base
assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator
end
@@ -630,10 +622,25 @@ module ApplicationTests
app "development"
assert_equal "", app.config.secret_token
- assert_equal nil, app.secrets.secret_key_base
- assert_raise ArgumentError, /\AA secret is required/ do
+ assert_nil app.secrets.secret_key_base
+ e = assert_raise ArgumentError do
app.key_generator
end
+ assert_match(/\AA secret is required/, e.message)
+ end
+
+ test "that nested keys are symbolized the same as parents for hashes more than one level deep" do
+ app_file "config/secrets.yml", <<-YAML
+ development:
+ smtp_settings:
+ address: "smtp.example.com"
+ user_name: "postmaster@example.com"
+ password: "697361616320736c6f616e2028656c6f7265737429"
+ YAML
+
+ app "development"
+
+ assert_equal "697361616320736c6f616e2028656c6f7265737429", app.secrets.smtp_settings[:password]
end
test "protect from forgery is the default in a new app" do
@@ -978,7 +985,7 @@ module ApplicationTests
class ::OmgController < ActionController::Base
def index
- render plain: env["action_dispatch.show_exceptions"]
+ render plain: request.env["action_dispatch.show_exceptions"]
end
end
@@ -1178,11 +1185,12 @@ module ApplicationTests
end
test "config.session_store with :active_record_store without activerecord-session_store gem" do
- assert_raise RuntimeError, /activerecord-session_store/ do
+ e = assert_raise RuntimeError do
make_basic_app do |application|
application.config.session_store :active_record_store
end
end
+ assert_match(/activerecord-session_store/, e.message)
end
test "default session store initializer does not overwrite the user defined session store even if it is disabled" do
@@ -1190,7 +1198,7 @@ module ApplicationTests
application.config.session_store :disabled
end
- assert_equal nil, app.config.session_store
+ assert_nil app.config.session_store
end
test "default session store initializer sets session store to cookie store" do
@@ -1518,5 +1526,24 @@ module ApplicationTests
assert_equal :default, Rails.configuration.debug_exception_response_format
end
+
+ test "controller force_ssl declaration can be used even if session_store is disabled" do
+ make_basic_app do |application|
+ application.config.session_store :disabled
+ end
+
+ class ::OmgController < ActionController::Base
+ force_ssl
+
+ def index
+ render plain: "Yay! You're on Rails!"
+ end
+ end
+
+ get "/"
+
+ assert_equal 301, last_response.status
+ assert_equal "https://example.org/", last_response.location
+ end
end
end
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index 0153f94504..fe581db286 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -169,5 +169,30 @@ module ApplicationTests
assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.text.erb"))
assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.html.erb"))
end
+
+ test "ARGV is mutated as expected" do
+ require "#{app_path}/config/environment"
+ Rails::Command.const_set("APP_PATH", "rails/all")
+
+ FileUtils.cd(rails_root) do
+ ARGV = ["mailer", "notifier", "foo"]
+ Rails::Command.const_set("ARGV", ARGV)
+ quietly { Rails::Command.invoke :generate, ARGV }
+
+ assert_equal ["notifier", "foo"], ARGV
+ end
+
+ Rails::Command.send(:remove_const, "APP_PATH")
+ end
+
+ test "help does not show hidden namespaces" do
+ FileUtils.cd(rails_root) do
+ output = `bin/rails generate --help`
+ assert_no_match "active_record:migration", output
+
+ output = `bin/rails destroy --help`
+ assert_no_match "active_record:migration", output
+ end
+ end
end
end
diff --git a/railties/test/application/help_test.rb b/railties/test/application/help_test.rb
new file mode 100644
index 0000000000..0c3fe8bfa3
--- /dev/null
+++ b/railties/test/application/help_test.rb
@@ -0,0 +1,23 @@
+require "isolation/abstract_unit"
+
+class HelpTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "command works" do
+ output = Dir.chdir(app_path) { `bin/rails help` }
+ assert_match "The most common rails commands are", output
+ end
+
+ test "short-cut alias works" do
+ output = Dir.chdir(app_path) { `bin/rails -h` }
+ assert_match "The most common rails commands are", output
+ end
+end
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index 32bce7d372..90927159dd 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -219,7 +219,7 @@ module ApplicationTests
end
require "#{app_path}/config/environment"
ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test.
- assert ActiveRecord::Base.connection.schema_cache.tables("posts")
+ assert ActiveRecord::Base.connection.schema_cache.data_sources("posts")
end
test "expire schema cache dump" do
@@ -227,10 +227,8 @@ module ApplicationTests
`rails generate model post title:string;
bin/rails db:migrate db:schema:cache:dump db:rollback`
end
- silence_warnings {
- require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection.schema_cache.tables("posts")
- }
+ require "#{app_path}/config/environment"
+ assert !ActiveRecord::Base.connection.schema_cache.data_sources("posts")
end
test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index 38f96f3ab9..c75a25bc6f 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -357,7 +357,7 @@ class LoadingTest < ActiveSupport::TestCase
assert Rails.application.initialized?
end
- protected
+ private
def setup_ar!
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index 790aca2aa4..c3a360e5d4 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -671,6 +671,40 @@ module ApplicationTests
assert_match %r[<p>Hello, World!</p>], last_response.body
end
+ test "multipart mailer preview with empty parts" do
+ mailer "notifier", <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template "notifier/foo", <<-RUBY
+ RUBY
+
+ html_template "notifier/foo", <<-RUBY
+ RUBY
+
+ mailer_preview "notifier", <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app("development")
+
+ get "/rails/mailers/notifier/foo?part=text/plain"
+ assert_equal 200, last_response.status
+
+ get "/rails/mailers/notifier/foo?part=text/html"
+ assert_equal 200, last_response.status
+ end
+
private
def build_app
super
diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb
index dc1d816dc5..93b5263bf7 100644
--- a/railties/test/application/middleware/cache_test.rb
+++ b/railties/test/application/middleware/cache_test.rb
@@ -117,12 +117,12 @@ module ApplicationTests
assert_equal "miss, store", last_response.headers["X-Rack-Cache"]
assert_equal "public", last_response.headers["Cache-Control"]
- body = last_response.body
etag = last_response.headers["ETag"]
- get "/expires/expires_etag", {}, "If-None-Match" => etag
+ get "/expires/expires_etag", {}, "HTTP_IF_NONE_MATCH" => etag
assert_equal "stale, valid, store", last_response.headers["X-Rack-Cache"]
- assert_equal body, last_response.body
+ assert_equal 304, last_response.status
+ assert_equal "", last_response.body
end
def test_cache_works_with_etags_private
@@ -137,7 +137,7 @@ module ApplicationTests
body = last_response.body
etag = last_response.headers["ETag"]
- get "/expires/expires_etag", { private: true }, "If-None-Match" => etag
+ get "/expires/expires_etag", { private: true }, "HTTP_IF_NONE_MATCH" => etag
assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_not_equal body, last_response.body
end
@@ -151,12 +151,12 @@ module ApplicationTests
assert_equal "miss, store", last_response.headers["X-Rack-Cache"]
assert_equal "public", last_response.headers["Cache-Control"]
- body = last_response.body
last = last_response.headers["Last-Modified"]
- get "/expires/expires_last_modified", {}, "If-Modified-Since" => last
+ get "/expires/expires_last_modified", {}, "HTTP_IF_MODIFIED_SINCE" => last
assert_equal "stale, valid, store", last_response.headers["X-Rack-Cache"]
- assert_equal body, last_response.body
+ assert_equal 304, last_response.status
+ assert_equal "", last_response.body
end
def test_cache_works_with_last_modified_private
@@ -171,7 +171,7 @@ module ApplicationTests
body = last_response.body
last = last_response.headers["Last-Modified"]
- get "/expires/expires_last_modified", { private: true }, "If-Modified-Since" => last
+ get "/expires/expires_last_modified", { private: true }, "HTTP_IF_MODIFIED_SINCE" => last
assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_not_equal body, last_response.body
end
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index 0e4acfdcec..959a629ede 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -173,7 +173,7 @@ module ApplicationTests
secret = app.key_generator.generate_key("encrypted cookie")
sign_secret = app.key_generator.generate_key("signed encrypted cookie")
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
get "/foo/read_raw_cookie"
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"]
@@ -222,7 +222,7 @@ module ApplicationTests
secret = app.key_generator.generate_key("encrypted cookie")
sign_secret = app.key_generator.generate_key("signed encrypted cookie")
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
get "/foo/read_raw_cookie"
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"]
@@ -281,7 +281,7 @@ module ApplicationTests
secret = app.key_generator.generate_key("encrypted cookie")
sign_secret = app.key_generator.generate_key("signed encrypted cookie")
- encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+ encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret)
get "/foo/read_raw_cookie"
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 9baf7360a5..0a6e5b52e9 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -30,10 +30,10 @@ module ApplicationTests
"Rack::Runtime",
"Rack::MethodOverride",
"ActionDispatch::RequestId",
- "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods
+ "ActionDispatch::RemoteIp",
+ "Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
- "ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
@@ -58,10 +58,10 @@ module ApplicationTests
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"ActionDispatch::RequestId",
- "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods
+ "ActionDispatch::RemoteIp",
+ "Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
- "ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"Rack::Head",
@@ -70,6 +70,37 @@ module ApplicationTests
], middleware
end
+ test "middleware dependencies" do
+ boot!
+
+ # The following array-of-arrays describes dependencies between
+ # middlewares: the first item in each list depends on the
+ # remaining items (and therefore must occur later in the
+ # middleware stack).
+
+ dependencies = [
+ # Logger needs a fully "corrected" request environment
+ %w(Rails::Rack::Logger Rack::MethodOverride ActionDispatch::RequestId ActionDispatch::RemoteIp),
+
+ # Serving public/ doesn't invoke user code, so it should skip
+ # locks etc
+ %w(ActionDispatch::Executor ActionDispatch::Static),
+
+ # Errors during reload must be reported
+ %w(ActionDispatch::Reloader ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions),
+
+ # Outright dependencies
+ %w(ActionDispatch::Static Rack::Sendfile),
+ %w(ActionDispatch::Flash ActionDispatch::Session::CookieStore),
+ %w(ActionDispatch::Session::CookieStore ActionDispatch::Cookies),
+ ]
+
+ require "tsort"
+ sorted = TSort.tsort((middleware | dependencies.flatten).method(:each),
+ lambda { |n, &b| dependencies.each { |m, *ds| ds.each(&b) if m == n } })
+ assert_equal sorted, middleware
+ end
+
test "Rack::Cache is not included by default" do
boot!
@@ -246,7 +277,7 @@ module ApplicationTests
get "/", {}, "HTTP_IF_NONE_MATCH" => etag
assert_equal 304, last_response.status
assert_equal "", last_response.body
- assert_equal nil, last_response.headers["Content-Type"]
+ assert_nil last_response.headers["Content-Type"]
assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"]
assert_equal etag, last_response.headers["Etag"]
@@ -255,7 +286,7 @@ module ApplicationTests
assert_equal "", last_response.body
assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"]
assert_equal "no-cache", last_response.headers["Cache-Control"]
- assert_equal nil, last_response.headers["Etag"]
+ assert_nil last_response.headers["Etag"]
end
test "ORIGINAL_FULLPATH is passed to env" do
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 51db634b75..c63f23fa0a 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -1,5 +1,4 @@
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
module ApplicationTests
module RakeTests
@@ -175,7 +174,7 @@ module ApplicationTests
`bin/rails generate model book title:string;
bin/rails db:migrate db:structure:dump`
structure_dump = File.read("db/structure.sql")
- assert_match(/CREATE TABLE \"books\"/, structure_dump)
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, structure_dump)
`bin/rails environment db:drop db:structure:load`
assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
@@ -203,7 +202,7 @@ module ApplicationTests
stderr_output = capture(:stderr) { `bin/rails db:structure:dump` }
assert_empty stderr_output
structure_dump = File.read("db/structure.sql")
- assert_match(/CREATE TABLE \"posts\"/, structure_dump)
+ assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"posts\"/, structure_dump)
end
end
diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb
index 7ac37b7700..40488a6aab 100644
--- a/railties/test/application/rake/framework_test.rb
+++ b/railties/test/application/rake/framework_test.rb
@@ -1,5 +1,4 @@
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
module ApplicationTests
module RakeTests
diff --git a/railties/test/application/rake/log_test.rb b/railties/test/application/rake/log_test.rb
new file mode 100644
index 0000000000..fdd3c71fe8
--- /dev/null
+++ b/railties/test/application/rake/log_test.rb
@@ -0,0 +1,33 @@
+require "isolation/abstract_unit"
+
+module ApplicationTests
+ module RakeTests
+ class LogTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "log:clear clear all environments log files by default" do
+ Dir.chdir(app_path) do
+ File.open("config/environments/staging.rb", "w")
+
+ File.write("log/staging.log", "staging")
+ File.write("log/test.log", "test")
+ File.write("log/dummy.log", "dummy")
+
+ `rails log:clear`
+
+ assert_equal 0, File.size("log/test.log")
+ assert_equal 0, File.size("log/staging.log")
+ assert_equal 5, File.size("log/dummy.log")
+ end
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index 76cb302c62..00f6620188 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -37,6 +37,22 @@ module ApplicationTests
end
end
+ test "migration with empty version" do
+ Dir.chdir(app_path) do
+ output = `bin/rails db:migrate VERSION= 2>&1`
+ assert_match(/Empty VERSION provided/, output)
+
+ output = `bin/rails db:migrate:redo VERSION= 2>&1`
+ assert_match(/Empty VERSION provided/, output)
+
+ output = `bin/rails db:migrate:up VERSION= 2>&1`
+ assert_match(/VERSION is required/, output)
+
+ output = `bin/rails db:migrate:down VERSION= 2>&1`
+ assert_match(/VERSION is required - To go down one migration, use db:rollback/, output)
+ end
+ end
+
test "model and migration generator with change syntax" do
Dir.chdir(app_path) do
`bin/rails generate model user username:string password:string;
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index cd09270df1..1b64a0a1ca 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -132,48 +132,6 @@ module ApplicationTests
assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
- def test_rails_routes_with_controller_environment
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
- end
- RUBY
-
- output = Dir.chdir(app_path) { `bin/rails routes CONTROLLER=cart` }
- assert_equal ["Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.",
- "Please use `bin/rails routes -c controller_name` instead.",
- "Prefix Verb URI Pattern Controller#Action",
- " cart GET /cart(.:format) cart#show\n"].join("\n"), output
-
- output = Dir.chdir(app_path) { `bin/rails routes -c cart` }
- 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_environment
- 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 = Dir.chdir(app_path) { `bin/rails routes -c Admin::PostController` }
- assert_equal expected_output, output
-
- output = Dir.chdir(app_path) { `bin/rails routes -c PostController` }
- assert_equal expected_output, output
- end
-
def test_singular_resource_output_in_rake_routes
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
@@ -232,6 +190,30 @@ module ApplicationTests
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 = Dir.chdir(app_path) { `bin/rails routes -c Admin::PostController` }
+ assert_equal expected_output, output
+
+ output = Dir.chdir(app_path) { `bin/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
@@ -341,16 +323,6 @@ module ApplicationTests
assert_no_match(/Errors running/, output)
end
- def test_db_test_clone_when_using_sql_format
- add_to_config "config.active_record.schema_format = :sql"
- output = Dir.chdir(app_path) do
- `bin/rails generate scaffold user username:string;
- bin/rails db:migrate;
- bin/rails db:test:clone 2>&1 --trace`
- end
- assert_match(/Execute db:test:clone_structure/, output)
- end
-
def test_db_test_prepare_when_using_sql_format
add_to_config "config.active_record.schema_format = :sql"
output = Dir.chdir(app_path) do
@@ -387,14 +359,14 @@ module ApplicationTests
bin/rails generate model product name:string;
bin/rails db:migrate db:schema:cache:dump`
end
- assert File.exist?(File.join(app_path, "db", "schema_cache.dump"))
+ assert File.exist?(File.join(app_path, "db", "schema_cache.yml"))
end
def test_rake_clear_schema_cache
Dir.chdir(app_path) do
`bin/rails db:schema:cache:dump db:schema:cache:clear`
end
- assert !File.exist?(File.join(app_path, "db", "schema_cache.dump"))
+ assert !File.exist?(File.join(app_path, "db", "schema_cache.yml"))
end
def test_copy_templates
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index c515e2b270..6742da20cc 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -263,7 +263,10 @@ module ApplicationTests
assert_equal "WIN", last_response.body
end
- { "development" => "baz", "production" => "bar" }.each do |mode, expected|
+ {
+ "development" => ["baz", "http://www.apple.com", "/dashboard"],
+ "production" => ["bar", "http://www.microsoft.com", "/profile"]
+ }.each do |mode, (expected_action, expected_url, expected_mapping)|
test "reloads routes when configuration is changed in #{mode}" do
controller :foo, <<-RUBY
class FooController < ApplicationController
@@ -274,12 +277,40 @@ module ApplicationTests
def baz
render plain: "baz"
end
+
+ def custom
+ render plain: custom_url
+ end
+
+ def mapping
+ render plain: url_for(User.new)
+ end
+ end
+ RUBY
+
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
+ end
end
RUBY
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#bar'
+ get 'custom', to: 'foo#custom'
+ get 'mapping', to: 'foo#mapping'
+
+ direct(:custom) { "http://www.microsoft.com" }
+ resolve("User") { "/profile" }
end
RUBY
@@ -288,16 +319,33 @@ module ApplicationTests
get "/foo"
assert_equal "bar", last_response.body
+ get "/custom"
+ assert_equal "http://www.microsoft.com", last_response.body
+
+ get "/mapping"
+ assert_equal "/profile", last_response.body
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#baz'
+ get 'custom', to: 'foo#custom'
+ get 'mapping', to: 'foo#mapping'
+
+ direct(:custom) { "http://www.apple.com" }
+ resolve("User") { "/dashboard" }
end
RUBY
sleep 0.1
get "/foo"
- assert_equal expected, last_response.body
+ assert_equal expected_action, last_response.body
+
+ get "/custom"
+ assert_equal expected_url, last_response.body
+
+ get "/mapping"
+ assert_equal expected_mapping, last_response.body
end
end
@@ -358,6 +406,14 @@ module ApplicationTests
def index
render plain: "foo"
end
+
+ def custom
+ render plain: custom_url
+ end
+
+ def mapping
+ render plain: url_for(User.new)
+ end
end
RUBY
@@ -369,6 +425,21 @@ module ApplicationTests
end
RUBY
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
+ end
+ end
+ RUBY
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
@@ -389,6 +460,12 @@ module ApplicationTests
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
get 'bar', to: 'bar#index'
+
+ get 'custom', to: 'foo#custom'
+ direct(:custom) { 'http://www.apple.com' }
+
+ get 'mapping', to: 'foo#mapping'
+ resolve('User') { '/profile' }
end
RUBY
@@ -402,6 +479,14 @@ module ApplicationTests
assert_equal "bar", last_response.body
assert_equal "/bar", Rails.application.routes.url_helpers.bar_path
+ get "/custom"
+ assert_equal "http://www.apple.com", last_response.body
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url
+
+ get "/mapping"
+ assert_equal "/profile", last_response.body
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
@@ -419,6 +504,18 @@ module ApplicationTests
assert_raises NoMethodError do
assert_equal "/bar", Rails.application.routes.url_helpers.bar_path
end
+
+ get "/custom"
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url
+ end
+
+ get "/mapping"
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
+ end
end
test "named routes are cleared when reloading" do
@@ -440,19 +537,41 @@ module ApplicationTests
end
RUBY
+ app_file "app/models/user.rb", <<-RUBY
+ class User
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def model_name
+ @_model_name ||= ActiveModel::Name.new(self.class, nil, "User")
+ end
+
+ def persisted?
+ false
+ end
+ end
+ RUBY
+
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':locale/foo', to: 'foo#index', as: 'foo'
+ get 'users', to: 'foo#users', as: 'users'
+ direct(:microsoft) { 'http://www.microsoft.com' }
+ resolve('User') { '/profile' }
end
RUBY
get "/en/foo"
assert_equal "foo", last_response.body
assert_equal "/en/foo", Rails.application.routes.url_helpers.foo_path(locale: "en")
+ assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get ':locale/bar', to: 'bar#index', as: 'foo'
+ get 'users', to: 'foo#users', as: 'users'
+ direct(:apple) { 'http://www.apple.com' }
end
RUBY
@@ -464,6 +583,12 @@ module ApplicationTests
get "/en/bar"
assert_equal "bar", last_response.body
assert_equal "/en/bar", Rails.application.routes.url_helpers.foo_path(locale: "en")
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.apple_url
+ assert_equal "/users", Rails.application.routes.url_helpers.polymorphic_path(User.new)
+
+ assert_raises NoMethodError do
+ assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url
+ end
end
test "resource routing with irregular inflection" do
@@ -493,5 +618,63 @@ module ApplicationTests
get "/yazilar"
assert_equal 200, last_response.status
end
+
+ test "reloading routes removes methods and doesn't undefine them" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/url', to: 'url#index'
+ end
+ RUBY
+
+ app_file "app/models/url_helpers.rb", <<-RUBY
+ module UrlHelpers
+ def foo_path
+ "/foo"
+ end
+ end
+ RUBY
+
+ app_file "app/models/context.rb", <<-RUBY
+ class Context
+ include UrlHelpers
+ include Rails.application.routes.url_helpers
+ end
+ RUBY
+
+ controller "url", <<-RUBY
+ class UrlController < ApplicationController
+ def index
+ context = Context.new
+ render plain: context.foo_path
+ end
+ end
+ RUBY
+
+ get "/url"
+ assert_equal "/foo", last_response.body
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/url', to: 'url#index'
+ get '/bar', to: 'foo#index', as: 'foo'
+ end
+ RUBY
+
+ Rails.application.reload_routes!
+
+ get "/url"
+ assert_equal "/bar", last_response.body
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/url', to: 'url#index'
+ end
+ RUBY
+
+ Rails.application.reload_routes!
+
+ get "/url"
+ assert_equal "/foo", last_response.body
+ end
end
end
diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb
index 7d058f6ee6..0c45bc398a 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -35,6 +35,14 @@ module ApplicationTests
assert_match "42", Dir.chdir(app_path) { `bin/rails runner "puts User.count"` }
end
+ def test_should_set_argv_when_running_code
+ output = Dir.chdir(app_path) {
+ # Both long and short args, at start and end of ARGV
+ `bin/rails runner "puts ARGV.join(',')" --foo a1 -b a2 a3 --moo`
+ }
+ assert_equal "--foo,a1,-b,a2,a3,--moo", output.chomp
+ end
+
def test_should_run_file
app_file "bin/count_users.rb", <<-SCRIPT
puts User.count
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 0939587960..a8e3a7ec5b 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -15,6 +15,16 @@ module ApplicationTests
teardown_app
end
+ def test_run_via_backwardscompatibility
+ require "rails/test_unit/minitest_plugin"
+
+ assert_nothing_raised do
+ Minitest.run_via[:ruby] = true
+ end
+
+ assert_predicate Minitest.run_via, :ruby?
+ end
+
def test_run_single_file
create_test_file :models, "foo"
create_test_file :models, "bar"
@@ -60,16 +70,18 @@ module ApplicationTests
end
def test_run_units
- skip "we no longer have the concept of unit tests. Just different directories..."
create_test_file :models, "foo"
create_test_file :helpers, "bar_helper"
create_test_file :unit, "baz_unit"
create_test_file :controllers, "foobar_controller"
- run_test_units_command.tap do |output|
- assert_match "FooTest", output
- assert_match "BarHelperTest", output
- assert_match "BazUnitTest", output
- assert_match "3 runs, 3 assertions, 0 failures", output
+
+ Dir.chdir(app_path) do
+ `bin/rails test:units`.tap do |output|
+ assert_match "FooTest", output
+ assert_match "BarHelperTest", output
+ assert_match "BazUnitTest", output
+ assert_match "3 runs, 3 assertions, 0 failures", output
+ end
end
end
@@ -107,16 +119,18 @@ module ApplicationTests
end
def test_run_functionals
- skip "we no longer have the concept of functional tests. Just different directories..."
create_test_file :mailers, "foo_mailer"
create_test_file :controllers, "bar_controller"
create_test_file :functional, "baz_functional"
create_test_file :models, "foo"
- run_test_functionals_command.tap do |output|
- assert_match "FooMailerTest", output
- assert_match "BarControllerTest", output
- assert_match "BazFunctionalTest", output
- assert_match "3 runs, 3 assertions, 0 failures", output
+
+ Dir.chdir(app_path) do
+ `bin/rails test:functionals`.tap do |output|
+ assert_match "FooMailerTest", output
+ assert_match "BarControllerTest", output
+ assert_match "BazFunctionalTest", output
+ assert_match "3 runs, 3 assertions, 0 failures", output
+ end
end
end
@@ -536,6 +550,106 @@ module ApplicationTests
assert_match "seed=1234", output, "passing TEST= should run selected test"
end
+ def test_rake_runs_multiple_test_tasks
+ create_test_file :models, "account"
+ create_test_file :controllers, "accounts_controller"
+ output = Dir.chdir(app_path) { `bin/rake test:models test:controllers TESTOPTS='-v'` }
+ assert_match "AccountTest#test_truth", output
+ assert_match "AccountsControllerTest#test_truth", output
+ end
+
+ def test_rake_db_and_test_tasks_parses_args_correctly
+ create_test_file :models, "account"
+ output = Dir.chdir(app_path) { `bin/rake db:migrate test:models TESTOPTS='-v' && echo ".tables" | rails dbconsole` }
+ assert_match "AccountTest#test_truth", output
+ assert_match "ar_internal_metadata", output
+ end
+
+ def test_warnings_option
+ app_file "test/models/warnings_test.rb", <<-RUBY
+ require 'test_helper'
+ def test_warnings
+ a = 1
+ end
+ RUBY
+ assert_match(/warning: assigned but unused variable/,
+ capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") })
+ end
+
+ def test_reset_sessions_before_rollback_on_system_tests
+ app_file "test/system/reset_session_before_rollback_test.rb", <<-RUBY
+ require "application_system_test_case"
+
+ class ResetSessionBeforeRollbackTest < ApplicationSystemTestCase
+ def teardown_fixtures
+ puts "rollback"
+ super
+ end
+
+ Capybara.singleton_class.prepend(Module.new do
+ def reset_sessions!
+ puts "reset sessions"
+ super
+ end
+ end)
+
+ test "dummy" do
+ end
+ end
+ RUBY
+
+ run_test_command("test/system/reset_session_before_rollback_test.rb").tap do |output|
+ assert_match "reset sessions\nrollback", output
+ assert_match "1 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output
+ end
+ end
+
+ def test_system_tests_are_not_run_with_the_default_test_command
+ app_file "test/system/dummy_test.rb", <<-RUBY
+ require "application_system_test_case"
+
+ class DummyTest < ApplicationSystemTestCase
+ test "something" do
+ assert true
+ end
+ end
+ RUBY
+
+ run_test_command("").tap do |output|
+ assert_match "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output
+ end
+ end
+
+ def test_system_tests_are_not_run_through_rake_test
+ app_file "test/system/dummy_test.rb", <<-RUBY
+ require "application_system_test_case"
+
+ class DummyTest < ApplicationSystemTestCase
+ test "something" do
+ assert true
+ end
+ end
+ RUBY
+
+ output = Dir.chdir(app_path) { `bin/rake test` }
+ assert_match "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output
+ end
+
+ def test_system_tests_are_run_through_rake_test_when_given_in_TEST
+ app_file "test/system/dummy_test.rb", <<-RUBY
+ require "application_system_test_case"
+
+ class DummyTest < ApplicationSystemTestCase
+ test "something" do
+ assert true
+ end
+ end
+ RUBY
+
+ output = Dir.chdir(app_path) { `bin/rake test TEST=test/system/dummy_test.rb` }
+ assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", output
+ end
+
private
def run_test_command(arguments = "test/unit/test_test.rb")
Dir.chdir(app_path) { `bin/rails t #{arguments}` }
diff --git a/railties/test/application/version_test.rb b/railties/test/application/version_test.rb
new file mode 100644
index 0000000000..6b419ae7ae
--- /dev/null
+++ b/railties/test/application/version_test.rb
@@ -0,0 +1,24 @@
+require "isolation/abstract_unit"
+require "rails/gem_version"
+
+class VersionTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "command works" do
+ output = Dir.chdir(app_path) { `bin/rails version` }
+ assert_equal "Rails #{Rails.gem_version}\n", output
+ end
+
+ test "short-cut alias works" do
+ output = Dir.chdir(app_path) { `bin/rails -v` }
+ assert_equal "Rails #{Rails.gem_version}\n", output
+ end
+end
diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb
index 17201d6f77..f71e56f323 100644
--- a/railties/test/backtrace_cleaner_test.rb
+++ b/railties/test/backtrace_cleaner_test.rb
@@ -15,7 +15,7 @@ class BacktraceCleanerTest < ActiveSupport::TestCase
test "should format installed gems not in Gem.default_dir correctly" do
target_dir = Gem.path.detect { |p| p != Gem.default_dir }
# skip this test if default_dir is the only directory on Gem.path
- if @target_dir
+ if target_dir
backtrace = [ "#{target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ]
result = @cleaner.clean(backtrace, :all)
assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0]
diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb
index 8a2f0294d0..1bd4225f34 100644
--- a/railties/test/code_statistics_calculator_test.rb
+++ b/railties/test/code_statistics_calculator_test.rb
@@ -24,7 +24,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
end
end
- test "count number of methods in MiniTest file" do
+ test "count number of methods in Minitest file" do
code = <<-RUBY
class FooTest < ActionController::TestCase
test 'expectation' do
diff --git a/railties/test/command/base_test.rb b/railties/test/command/base_test.rb
new file mode 100644
index 0000000000..ebfc4d0ba9
--- /dev/null
+++ b/railties/test/command/base_test.rb
@@ -0,0 +1,11 @@
+require "abstract_unit"
+require "rails/command"
+require "rails/commands/generate/generate_command"
+require "rails/commands/secrets/secrets_command"
+
+class Rails::Command::BaseTest < ActiveSupport::TestCase
+ test "printing commands" do
+ assert_equal %w(generate), Rails::Command::GenerateCommand.printing_commands
+ assert_equal %w(secrets:setup secrets:edit), Rails::Command::SecretsCommand.printing_commands
+ end
+end
diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb
new file mode 100644
index 0000000000..00b0343397
--- /dev/null
+++ b/railties/test/commands/secrets_test.rb
@@ -0,0 +1,37 @@
+require "isolation/abstract_unit"
+require "rails/command"
+require "rails/commands/secrets/secrets_command"
+
+class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "edit without editor gives hint" do
+ assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "")
+ end
+
+ test "edit secrets" do
+ run_setup_command
+
+ # Run twice to ensure encrypted secrets can be reread after first edit pass.
+ 2.times do
+ assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289…/, run_edit_command)
+ end
+ end
+
+ private
+ def run_edit_command(editor: "cat")
+ Dir.chdir(app_path) { `EDITOR="#{editor}" bin/rails secrets:edit` }
+ end
+
+ def run_setup_command
+ Dir.chdir(app_path) { `bin/rails secrets:setup` }
+ end
+end
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index 391886bf33..2d1f071969 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -7,31 +7,35 @@ class Rails::ServerTest < ActiveSupport::TestCase
include EnvHelpers
def test_environment_with_server_option
- args = ["thin", "-e", "production"]
- options = Rails::Server::Options.new.parse!(args)
+ args = ["thin", "-e", "production"]
+ options = parse_arguments(args)
assert_equal "production", options[:environment]
assert_equal "thin", options[:server]
end
def test_environment_without_server_option
- args = ["-e", "production"]
- options = Rails::Server::Options.new.parse!(args)
+ args = ["-e", "production"]
+ options = parse_arguments(args)
assert_equal "production", options[:environment]
assert_nil options[:server]
end
def test_server_option_without_environment
- args = ["thin"]
- options = Rails::Server::Options.new.parse!(args)
- assert_nil options[:environment]
- assert_equal "thin", options[:server]
+ args = ["thin"]
+ with_rack_env nil do
+ with_rails_env nil do
+ options = parse_arguments(args)
+ assert_equal "development", options[:environment]
+ assert_equal "thin", options[:server]
+ end
+ end
end
def test_environment_with_rails_env
with_rack_env nil do
with_rails_env "production" do
- server = Rails::Server.new
- assert_equal "production", server.options[:environment]
+ options = parse_arguments
+ assert_equal "production", options[:environment]
end
end
end
@@ -39,40 +43,39 @@ class Rails::ServerTest < ActiveSupport::TestCase
def test_environment_with_rack_env
with_rails_env nil do
with_rack_env "production" do
- server = Rails::Server.new
- assert_equal "production", server.options[:environment]
+ options = parse_arguments
+ assert_equal "production", options[:environment]
end
end
end
def test_environment_with_port
switch_env "PORT", "1234" do
- server = Rails::Server.new
- assert_equal 1234, server.options[:Port]
+ options = parse_arguments
+ assert_equal 1234, options[:Port]
end
end
def test_environment_with_host
switch_env "HOST", "1.2.3.4" do
- server = Rails::Server.new
- assert_equal "1.2.3.4", server.options[:Host]
+ options = parse_arguments
+ assert_equal "1.2.3.4", options[:Host]
end
end
def test_caching_without_option
args = []
- options = Rails::Server::Options.new.parse!(args)
- merged_options = Rails::Server.new.default_options.merge(options)
- assert_equal nil, merged_options[:caching]
+ options = parse_arguments(args)
+ assert_nil options[:caching]
end
def test_caching_with_option
args = ["--dev-caching"]
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal true, options[:caching]
args = ["--no-dev-caching"]
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal false, options[:caching]
end
@@ -80,44 +83,78 @@ class Rails::ServerTest < ActiveSupport::TestCase
with_rack_env nil do
with_rails_env nil do
args = []
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal true, options[:log_stdout]
args = ["-e", "development"]
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal true, options[:log_stdout]
args = ["-e", "production"]
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal false, options[:log_stdout]
with_rack_env "development" do
args = []
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal true, options[:log_stdout]
end
with_rack_env "production" do
args = []
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal false, options[:log_stdout]
end
with_rails_env "development" do
args = []
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal true, options[:log_stdout]
end
with_rails_env "production" do
args = []
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal false, options[:log_stdout]
end
end
end
end
+ def test_host
+ with_rails_env "development" do
+ options = parse_arguments([])
+ assert_equal "localhost", options[:Host]
+ end
+
+ with_rails_env "production" do
+ options = parse_arguments([])
+ assert_equal "0.0.0.0", options[:Host]
+ end
+
+ with_rails_env "development" do
+ args = ["-b", "127.0.0.1"]
+ options = parse_arguments(args)
+ assert_equal "127.0.0.1", options[:Host]
+ end
+ end
+
+ def test_argument_precedence_over_environment_variable
+ switch_env "HOST", "1.2.3.4" do
+ args = ["-b", "127.0.0.1"]
+ options = parse_arguments(args)
+ assert_equal "127.0.0.1", options[:Host]
+ end
+ end
+
+ def test_records_user_supplied_options
+ server_options = parse_arguments(["-p", 3001])
+ assert_equal [:Port], server_options[:user_supplied_options]
+
+ server_options = parse_arguments(["--port", 3001])
+ assert_equal [:Port], server_options[:user_supplied_options]
+ end
+
def test_default_options
server = Rails::Server.new
old_default_options = server.default_options
@@ -129,15 +166,19 @@ class Rails::ServerTest < ActiveSupport::TestCase
def test_restart_command_contains_customized_options
original_args = ARGV.dup
- args = ["-p", "4567"]
+ args = %w(-p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C)
ARGV.replace args
- options = Rails::Server::Options.new.parse! args
- server = Rails::Server.new options
- expected = "bin/rails server -p 4567"
+ options = parse_arguments(args)
+ expected = "bin/rails server -p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C"
- assert_equal expected, server.default_options[:restart_cmd]
+ assert_equal expected, options[:restart_cmd]
ensure
ARGV.replace original_args
end
+
+ private
+ def parse_arguments(args = [])
+ Rails::Command::ServerCommand.new([], args).server_options
+ end
end
diff --git a/railties/test/engine/commands_tasks_test.rb b/railties/test/engine/commands_tasks_test.rb
deleted file mode 100644
index 817175b9ef..0000000000
--- a/railties/test/engine/commands_tasks_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require "abstract_unit"
-
-class Rails::Engine::CommandsTasksTest < ActiveSupport::TestCase
- def setup
- @destination_root = Dir.mktmpdir("bukkits")
- Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` }
- end
-
- def teardown
- FileUtils.rm_rf(@destination_root)
- end
-
- def test_help_command_work_inside_engine
- output = capture(:stderr) do
- Dir.chdir(plugin_path) { `bin/rails --help` }
- end
- assert_no_match "NameError", output
- end
-
- private
- def plugin_path
- "#{@destination_root}/bukkits"
- end
-end
diff --git a/railties/test/engine/commands_test.rb b/railties/test/engine/commands_test.rb
new file mode 100644
index 0000000000..b1c937143f
--- /dev/null
+++ b/railties/test/engine/commands_test.rb
@@ -0,0 +1,96 @@
+require "abstract_unit"
+begin
+ require "pty"
+rescue LoadError
+end
+
+class Rails::Engine::CommandsTest < ActiveSupport::TestCase
+ def setup
+ @destination_root = Dir.mktmpdir("bukkits")
+ Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` }
+ end
+
+ def teardown
+ FileUtils.rm_rf(@destination_root)
+ end
+
+ def test_help_command_work_inside_engine
+ output = capture(:stderr) do
+ Dir.chdir(plugin_path) { `bin/rails --help` }
+ end
+ assert_no_match "NameError", output
+ end
+
+ def test_runner_command_work_inside_engine
+ output = capture(:stdout) do
+ Dir.chdir(plugin_path) { system("bin/rails runner 'puts Rails.env'") }
+ end
+
+ assert_equal "test", output.strip
+ end
+
+ def test_console_command_work_inside_engine
+ skip "PTY unavailable" unless available_pty?
+
+ master, slave = PTY.open
+ spawn_command("console", slave)
+ assert_output(">", master)
+ ensure
+ master.puts "quit"
+ end
+
+ def test_dbconsole_command_work_inside_engine
+ skip "PTY unavailable" unless available_pty?
+
+ master, slave = PTY.open
+ spawn_command("dbconsole", slave)
+ assert_output("sqlite>", master)
+ ensure
+ master.puts ".exit"
+ end
+
+ def test_server_command_work_inside_engine
+ skip "PTY unavailable" unless available_pty?
+
+ master, slave = PTY.open
+ pid = spawn_command("server", slave)
+ assert_output("Listening on", master)
+ ensure
+ kill(pid)
+ end
+
+ private
+ def plugin_path
+ "#{@destination_root}/bukkits"
+ end
+
+ def assert_output(expected, io, timeout = 10)
+ timeout = Time.now + timeout
+
+ output = ""
+ until output.include?(expected) || Time.now > timeout
+ if IO.select([io], [], [], 0.1)
+ output << io.read(1)
+ end
+ end
+
+ assert_includes output, expected, "#{expected.inspect} expected, but got:\n\n#{output}"
+ end
+
+ def spawn_command(command, fd)
+ Process.spawn(
+ "#{plugin_path}/bin/rails #{command}",
+ in: fd, out: fd, err: fd
+ )
+ end
+
+ def available_pty?
+ defined?(PTY) && PTY.respond_to?(:open)
+ end
+
+ def kill(pid)
+ Process.kill("TERM", pid)
+ Process.wait(pid)
+ rescue Errno::ESRCH
+ end
+end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 0a26897a4d..360e8e97d7 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -369,7 +369,7 @@ F
assert_equal("", action(:log, :yes, "YES"))
end
- protected
+ private
def action(*args, &block)
capture(:stdout) { generator.send(*args, &block) }
diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb
index bbb814ef4e..2edb39c8e8 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -35,7 +35,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_no_match(/gem 'coffee-rails'/, content)
- assert_no_match(/gem 'jquery-rails'/, content)
assert_no_match(/gem 'sass-rails'/, content)
assert_no_match(/gem 'web-console'/, content)
assert_match(/# gem 'jbuilder'/, content)
@@ -62,13 +61,26 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_generator_skips_per_form_csrf_token_and_origin_check_configs_for_api_apps
+ def test_app_update_does_not_generate_unnecessary_config_files
run_generator
- assert_file "config/initializers/new_framework_defaults.rb" do |initializer_content|
- assert_no_match(/per_form_csrf_tokens/, initializer_content)
- assert_no_match(/forgery_protection_origin_check/, initializer_content)
- end
+ generator = Rails::Generators::AppGenerator.new ["rails"],
+ { api: true, update: true }, destination_root: destination_root, shell: @shell
+ quietly { generator.send(:update_config_files) }
+
+ assert_no_file "config/initializers/cookies_serializer.rb"
+ assert_no_file "config/initializers/assets.rb"
+ assert_no_file "config/initializers/new_framework_defaults_5_1.rb"
+ end
+
+ def test_app_update_does_not_generate_unnecessary_bin_files
+ run_generator
+
+ generator = Rails::Generators::AppGenerator.new ["rails"],
+ { api: true, update: true }, destination_root: destination_root, shell: @shell
+ quietly { generator.send(:update_bin_files) }
+
+ assert_no_file "bin/yarn"
end
private
@@ -106,10 +118,10 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
%w(app/assets
app/helpers
app/views/layouts/application.html.erb
+ bin/yarn
config/initializers/assets.rb
config/initializers/cookies_serializer.rb
lib/assets
- vendor/assets
test/helpers
tmp/cache/assets
public/404.html
@@ -117,6 +129,8 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
public/500.html
public/apple-touch-icon-precomposed.png
public/apple-touch-icon.png
- public/favicon.ico)
+ public/favicon.icon
+ package.json
+ )
end
end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 3ec99193e3..7965bd68d0 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -42,10 +42,8 @@ DEFAULT_APP_FILES = %w(
test/helpers
test/mailers
test/integration
+ test/system
vendor
- vendor/assets
- vendor/assets/stylesheets
- vendor/assets/javascripts
tmp
tmp/cache
tmp/cache/assets
@@ -135,7 +133,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_rails_update_generates_correct_session_key
+ def test_app_update_generates_correct_session_key
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
@@ -158,67 +156,65 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_file "config/initializers/cors.rb"
end
- def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured
- app_root = File.join(destination_root, "myapp")
+ def test_new_application_doesnt_need_defaults
+ assert_no_file "config/initializers/new_framework_defaults_5_1.rb"
+ end
+
+ def test_new_application_load_defaults
+ app_root = File.join(destination_root, "myfirstapp")
run_generator [app_root]
+ output = nil
- stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ Dir.chdir(app_root) do
+ output = `./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"`
end
+
+ assert_equal "false\n", output
end
- def test_rails_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured
+ def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb")
-
stub_rails_application(app_root) do
generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb",
- /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
end
end
- def test_rails_update_dont_set_file_watcher
+ def test_app_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
+ FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb")
+
stub_rails_application(app_root) do
generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/environments/development.rb" do |content|
- assert_match(/# config.file_watcher/, content)
- end
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb",
+ /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
end
end
- def test_rails_update_does_not_create_new_framework_defaults_by_default
+ def test_app_update_create_new_framework_defaults
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- FileUtils.rm("#{app_root}/config/initializers/new_framework_defaults.rb")
+ assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_1.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.rb" do |content|
- assert_match(/ActiveSupport\.halt_callback_chains_on_return_false = true/, content)
- assert_match(/Rails\.application\.config.active_record\.belongs_to_required_by_default = false/, content)
- assert_no_match(/Rails\.application\.config\.ssl_options/, content)
- end
+ assert_file "#{app_root}/config/initializers/new_framework_defaults_5_1.rb"
end
end
- def test_rails_update_does_not_create_rack_cors
+ def test_app_update_does_not_create_rack_cors
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
@@ -230,7 +226,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_rails_update_does_not_remove_rack_cors_if_already_present
+ def test_app_update_does_not_remove_rack_cors_if_already_present
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
@@ -338,12 +334,13 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
assert_file "config/environments/production.rb" do |content|
assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content)
+ assert_match(/^ config\.read_encrypted_secrets = true/, content)
end
end
def test_generator_defaults_to_puma_version
run_generator [destination_root]
- assert_gem "puma", "'~> 3.0'"
+ assert_gem "puma", "'~> 3.7'"
end
def test_generator_if_skip_puma_is_given
@@ -369,10 +366,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "bin/update" do |update_content|
assert_no_match(/db:migrate/, update_content)
end
-
- assert_file "config/initializers/new_framework_defaults.rb" do |initializer_content|
- assert_no_match(/belongs_to_required_by_default/, initializer_content)
- end
end
def test_generator_if_skip_action_mailer_is_given
@@ -405,7 +398,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content)
end
assert_file "Gemfile" do |content|
- assert_no_match(/jquery-rails/, content)
assert_no_match(/sass-rails/, content)
assert_no_match(/uglifier/, content)
assert_no_match(/coffee-rails/, content)
@@ -418,9 +410,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/config\.assets\.js_compressor = :uglifier/, content)
assert_no_match(/config\.assets\.css_compressor = :sass/, content)
end
- assert_file "config/initializers/new_framework_defaults.rb" do |content|
- assert_no_match(/unknown_asset_fallback/, content)
- end
+ assert_no_file "config/initializers/new_framework_defaults_5_1.rb"
+ end
+
+ def test_generator_if_skip_yarn_is_given
+ run_generator [destination_root, "--skip-yarn"]
+
+ assert_no_file "package.json"
+ assert_no_file "bin/yarn"
end
def test_generator_if_skip_action_cable_is_given
@@ -439,6 +436,30 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile", /^# gem 'redis'/
end
+ def test_generator_if_skip_test_is_given
+ run_generator [destination_root, "--skip-test"]
+ assert_file "Gemfile" do |content|
+ assert_no_match(/capybara/, content)
+ assert_no_match(/selenium-webdriver/, content)
+ end
+ 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
+ end
+
+ def test_generator_if_api_is_given
+ run_generator [destination_root, "--api"]
+ assert_file "Gemfile" do |content|
+ assert_no_match(/capybara/, content)
+ assert_no_match(/selenium-webdriver/, content)
+ end
+ end
+
def test_inclusion_of_javascript_runtime
run_generator
if defined?(JRUBY_VERSION)
@@ -448,29 +469,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_jquery_is_the_default_javascript_library
+ def test_rails_ujs_is_the_default_ujs_library
run_generator
assert_file "app/assets/javascripts/application.js" do |contents|
- assert_match %r{^//= require jquery}, contents
- assert_match %r{^//= require jquery_ujs}, contents
+ assert_match %r{^//= require rails-ujs}, contents
end
- assert_gem "jquery-rails"
- end
-
- def test_other_javascript_libraries
- run_generator [destination_root, "-j", "prototype"]
- assert_file "app/assets/javascripts/application.js" do |contents|
- assert_match %r{^//= require prototype}, contents
- assert_match %r{^//= require prototype_ujs}, contents
- end
- assert_gem "prototype-rails"
end
def test_javascript_is_skipped_if_required
run_generator [destination_root, "--skip-javascript"]
assert_no_file "app/assets/javascripts"
- assert_no_file "vendor/assets/javascripts"
assert_file "app/views/layouts/application.html.erb" do |contents|
assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents)
@@ -479,7 +488,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_no_match(/coffee-rails/, content)
- assert_no_match(/jquery-rails/, content)
assert_no_match(/uglifier/, content)
end
@@ -493,11 +501,30 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_no_match(/coffee-rails/, content)
- assert_match(/jquery-rails/, content)
assert_match(/uglifier/, content)
end
end
+ def test_generator_for_yarn
+ run_generator([destination_root])
+ assert_file "package.json", /dependencies/
+ assert_file "config/initializers/assets.rb", /node_modules/
+ end
+
+ def test_generator_for_yarn_skipped
+ run_generator([destination_root, "--skip-yarn"])
+ assert_no_file "package.json"
+
+ assert_file "config/initializers/assets.rb" do |content|
+ assert_no_match(/node_modules/, content)
+ end
+
+ assert_file ".gitignore" do |content|
+ assert_no_match(/node_modules/, content)
+ assert_no_match(/yarn-error\.log/, content)
+ end
+ end
+
def test_inclusion_of_jbuilder
run_generator
assert_gem "jbuilder"
@@ -580,6 +607,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_pretend_option
output = run_generator [File.join(destination_root, "myapp"), "--pretend"]
assert_no_match(/run bundle install/, output)
+ assert_no_match(/run git init/, output)
end
def test_application_name_with_spaces
@@ -719,6 +747,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_version_control_initializes_git_repo
+ run_generator [destination_root]
+ assert_directory ".git"
+ end
+
def test_create_keeps
run_generator
folders_with_keep = %w(
@@ -736,7 +769,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
test/helpers
test/integration
tmp
- vendor/assets/stylesheets
)
folders_with_keep.each do |folder|
assert_file("#{folder}/.keep")
@@ -766,7 +798,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
template
end
- sequence = ["install", "exec spring binstub --all", "echo ran after_bundle"]
+ sequence = ["git init", "install", "exec spring binstub --all", "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}"
@@ -781,11 +813,29 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- assert_equal 3, @sequence_step
+ assert_equal 4, @sequence_step
end
- protected
+ def test_system_tests_directory_generated
+ run_generator
+
+ assert_file("test/system/.keep")
+ assert_directory("test/system")
+ end
+
+ def test_system_tests_are_not_generated_on_system_test_skip
+ run_generator [destination_root, "--skip-system-test"]
+
+ assert_no_directory("test/system")
+ end
+
+ def test_system_tests_are_not_generated_on_test_skip
+ run_generator [destination_root, "--skip-test"]
+
+ assert_no_directory("test/system")
+ end
+ private
def stub_rails_application(root)
Rails.application.config.root = root
Rails.application.class.stub(:name, "Myapp") do
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index 9b986a636a..af86a0136f 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -65,7 +65,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_add_routes
run_generator
- assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
+ assert_file "config/routes.rb", /^ get 'account\/foo'/, /^ get 'account\/bar'/
end
def test_skip_routes
diff --git a/railties/test/generators/encrypted_secrets_generator_test.rb b/railties/test/generators/encrypted_secrets_generator_test.rb
new file mode 100644
index 0000000000..21fdcab19f
--- /dev/null
+++ b/railties/test/generators/encrypted_secrets_generator_test.rb
@@ -0,0 +1,42 @@
+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/generator_test.rb b/railties/test/generators/generator_test.rb
index 904bade658..4444b3a56e 100644
--- a/railties/test/generators/generator_test.rb
+++ b/railties/test/generators/generator_test.rb
@@ -88,12 +88,12 @@ module Rails
specifier_for = -> v { generator.send(:rails_version_specifier, Gem::Version.new(v)) }
assert_equal "~> 4.1.13", specifier_for["4.1.13"]
- assert_equal [">= 4.1.6.rc1", "< 4.2"], specifier_for["4.1.6.rc1"]
+ assert_equal "~> 4.1.6.rc1", specifier_for["4.1.6.rc1"]
assert_equal ["~> 4.1.7", ">= 4.1.7.1"], specifier_for["4.1.7.1"]
assert_equal ["~> 4.1.7", ">= 4.1.7.1.2"], specifier_for["4.1.7.1.2"]
- assert_equal [">= 4.1.7.1.rc2", "< 4.2"], specifier_for["4.1.7.1.rc2"]
- assert_equal [">= 4.2.0.beta1", "< 4.3"], specifier_for["4.2.0.beta1"]
- assert_equal [">= 5.0.0.beta1", "< 5.1"], specifier_for["5.0.0.beta1"]
+ assert_equal ["~> 4.1.7", ">= 4.1.7.1.rc2"], specifier_for["4.1.7.1.rc2"]
+ assert_equal "~> 4.2.0.beta1", specifier_for["4.2.0.beta1"]
+ assert_equal "~> 5.0.0.beta1", specifier_for["5.0.0.beta1"]
end
end
end
diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb
index 8bcc02440a..9358b63bd4 100644
--- a/railties/test/generators/integration_test_generator_test.rb
+++ b/railties/test/generators/integration_test_generator_test.rb
@@ -3,10 +3,14 @@ require "rails/generators/rails/integration_test/integration_test_generator"
class IntegrationTestGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
- arguments %w(integration)
def test_integration_test_skeleton_is_created
- run_generator
+ run_generator %w(integration)
assert_file "test/integration/integration_test.rb", /class IntegrationTest < ActionDispatch::IntegrationTest/
end
+
+ def test_namespaced_integration_test_skeleton_is_created
+ run_generator %w(iguchi/integration)
+ assert_file "test/integration/iguchi/integration_test.rb", /class Iguchi::IntegrationTest < ActionDispatch::IntegrationTest/
+ end
end
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index 6e1d1b70a9..f46278cefe 100644
--- a/railties/test/generators/migration_generator_test.rb
+++ b/railties/test/generators/migration_generator_test.rb
@@ -309,6 +309,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_add_migration_to_configured_path
+ old_paths = Rails.application.config.paths["db/migrate"]
+ Rails.application.config.paths.add "db/migrate", with: "db2/migrate"
+
+ migration = "migration_in_custom_path"
+ run_generator [migration]
+ assert_migration "db2/migrate/#{migration}.rb", /.*/
+ ensure
+ Rails.application.config.paths["db/migrate"] = old_paths
+ end
+
private
def with_singular_table_name
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 2b9f3ed7f2..99490af3a9 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -220,6 +220,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase
ActiveRecord::Base.timestamped_migrations = true
end
+ def test_migration_with_configured_path
+ old_paths = Rails.application.config.paths["db/migrate"]
+ Rails.application.config.paths.add "db/migrate", with: "db2/migrate"
+
+ run_generator
+
+ assert_migration "db2/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/
+ ensure
+ Rails.application.config.paths["db/migrate"] = old_paths
+ end
+
def test_model_with_references_attribute_generates_belongs_to_associations
run_generator ["product", "name:string", "supplier:references"]
assert_file "app/models/product.rb", /belongs_to :supplier/
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index 0258b3b9d7..3015b5363b 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -131,7 +131,7 @@ class NamedBaseTest < Rails::Generators::TestCase
assert_name g, "admin.foos", :controller_i18n_scope
end
- protected
+ private
def assert_name(generator, value, method)
assert_equal value, generator.send(method)
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 0fdc30ac43..afb37b6a99 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -1,6 +1,7 @@
require "generators/generators_test_helper"
require "rails/generators/rails/plugin/plugin_generator"
require "generators/shared_generator_tests"
+require "rails/engine/updater"
DEFAULT_PLUGIN_FILES = %w(
.gitignore
@@ -421,6 +422,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--full"]
assert_file "bin/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/
assert_file "bin/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/
+ assert_file "bin/rails", %r|APP_PATH = File.expand_path\('../../test/dummy/config/application', __FILE__\)|
assert_file "bin/rails", /require 'rails\/all'/
assert_file "bin/rails", /require 'rails\/engine\/commands'/
end
@@ -490,6 +492,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_no_directory "test/dummy/doc"
assert_no_directory "test/dummy/test"
assert_no_directory "test/dummy/vendor"
+ assert_no_directory "test/dummy/.git"
end
def test_skipping_test_files
@@ -507,7 +510,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_no_match("gemspec", contents)
assert_match(/gem 'rails'/, contents)
assert_match_sqlite3(contents)
- assert_no_match(/# gem "jquery-rails"/, contents)
end
end
@@ -535,6 +537,21 @@ class PluginGeneratorTest < Rails::Generators::TestCase
FileUtils.rm gemfile_path
end
+ def test_creating_plugin_only_specify_plugin_name_in_app_directory_adds_gemfile_entry
+ # simulate application existence
+ gemfile_path = "#{Rails.root}/Gemfile"
+ Object.const_set("APP_PATH", Rails.root)
+ FileUtils.touch gemfile_path
+
+ FileUtils.cd(destination_root)
+ run_generator ["bukkits"]
+
+ assert_file gemfile_path, /gem 'bukkits', path: 'bukkits'/
+ ensure
+ Object.send(:remove_const, "APP_PATH")
+ FileUtils.rm gemfile_path
+ end
+
def test_skipping_gemfile_entry
# simulate application existence
gemfile_path = "#{Rails.root}/Gemfile"
@@ -715,7 +732,22 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
- protected
+ def test_app_update_generates_bin_file
+ run_generator [destination_root, "--mountable"]
+
+ Object.const_set("ENGINE_ROOT", destination_root)
+ FileUtils.rm("#{destination_root}/bin/rails")
+
+ quietly { Rails::Engine::Updater.run(:create_bin_files) }
+
+ assert_file "#{destination_root}/bin/rails" do |content|
+ assert_match(%r|APP_PATH = File\.expand_path\('\.\./\.\./test/dummy/config/application', __FILE__\)|, content)
+ end
+ ensure
+ Object.send(:remove_const, "ENGINE_ROOT")
+ end
+
+ private
def action(*args, &block)
silence(:stdout) { generator.send(*args, &block) }
diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb
index 0bdf3b2726..0bdd2a77d2 100644
--- a/railties/test/generators/plugin_test_runner_test.rb
+++ b/railties/test/generators/plugin_test_runner_test.rb
@@ -92,6 +92,17 @@ class PluginTestRunnerTest < ActiveSupport::TestCase
assert_equal 1, result.scan(/1 runs, 1 assertions, 0 failures/).length
end
+ def test_warnings_option
+ plugin_file "test/models/warnings_test.rb", <<-RUBY
+ require 'test_helper'
+ def test_warnings
+ a = 1
+ end
+ RUBY
+ assert_match(/warning: assigned but unused variable/,
+ capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") })
+ end
+
private
def plugin_path
"#{@destination_root}/bukkits"
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index bd23faf268..9971626f9f 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -230,6 +230,12 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/@user\.destroy/, m)
end
end
+
+ assert_no_file "app/views/users/index.html.erb"
+ assert_no_file "app/views/users/edit.html.erb"
+ assert_no_file "app/views/users/show.html.erb"
+ assert_no_file "app/views/users/new.html.erb"
+ assert_no_file "app/views/users/_form.html.erb"
end
def test_api_controller_tests
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index 6b7e2c91d7..bc76cead18 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -62,6 +62,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test)
end
+ # System tests
+ assert_file "test/system/product_lines_test.rb" do |test|
+ assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, test)
+ end
+
# Views
assert_no_file "app/views/layouts/product_lines.html.erb"
@@ -432,8 +437,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "app/views/accounts/_form.html.erb" do |content|
- assert_match(/^\W{4}<%= f\.text_field :name %>/, content)
- assert_match(/^\W{4}<%= f\.text_field :currency_id %>/, 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)
end
end
@@ -456,8 +461,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
assert_file "app/views/users/_form.html.erb" do |content|
- assert_match(/<%= f\.password_field :password %>/, content)
- assert_match(/<%= f\.password_field :password_confirmation %>/, content)
+ assert_match(/<%= form\.password_field :password, id: :user_password %>/, content)
+ assert_match(/<%= form\.password_field :password_confirmation, id: :user_password_confirmation %>/, content)
end
assert_file "app/views/users/index.html.erb" do |content|
@@ -492,6 +497,26 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_scaffold_tests_pass_by_default_inside_namespaced_mountable_engine
+ Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits-admin --mountable` }
+
+ engine_path = File.join(destination_root, "bukkits-admin")
+
+ Dir.chdir(engine_path) do
+ quietly do
+ `bin/rails g scaffold User name:string age:integer;
+ bin/rails db:migrate`
+ end
+
+ assert_file "bukkits-admin/app/controllers/bukkits/admin/users_controller.rb" do |content|
+ assert_match(/module Bukkits::Admin/, content)
+ assert_match(/class UsersController < ApplicationController/, content)
+ end
+
+ assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
+ end
+ end
+
def test_scaffold_tests_pass_by_default_inside_full_engine
Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full` }
@@ -533,4 +558,59 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
+
+ def test_scaffold_on_invoke_inside_mountable_engine
+ Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` }
+ engine_path = File.join(destination_root, "bukkits")
+
+ Dir.chdir(engine_path) do
+ quietly { `bin/rails generate scaffold User name:string age:integer` }
+
+ assert File.exist?("app/models/bukkits/user.rb")
+ assert File.exist?("test/models/bukkits/user_test.rb")
+ assert File.exist?("test/fixtures/bukkits/users.yml")
+
+ assert File.exist?("app/controllers/bukkits/users_controller.rb")
+ assert File.exist?("test/controllers/bukkits/users_controller_test.rb")
+
+ assert File.exist?("app/views/bukkits/users/index.html.erb")
+ assert File.exist?("app/views/bukkits/users/edit.html.erb")
+ assert File.exist?("app/views/bukkits/users/show.html.erb")
+ assert File.exist?("app/views/bukkits/users/new.html.erb")
+ assert File.exist?("app/views/bukkits/users/_form.html.erb")
+
+ assert File.exist?("app/helpers/bukkits/users_helper.rb")
+
+ assert File.exist?("app/assets/javascripts/bukkits/users.js")
+ assert File.exist?("app/assets/stylesheets/bukkits/users.css")
+ end
+ end
+
+ def test_scaffold_on_revoke_inside_mountable_engine
+ Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` }
+ engine_path = File.join(destination_root, "bukkits")
+
+ Dir.chdir(engine_path) do
+ quietly { `bin/rails generate scaffold User name:string age:integer` }
+ quietly { `bin/rails destroy scaffold User` }
+
+ assert_not File.exist?("app/models/bukkits/user.rb")
+ assert_not File.exist?("test/models/bukkits/user_test.rb")
+ assert_not File.exist?("test/fixtures/bukkits/users.yml")
+
+ assert_not File.exist?("app/controllers/bukkits/users_controller.rb")
+ assert_not File.exist?("test/controllers/bukkits/users_controller_test.rb")
+
+ assert_not File.exist?("app/views/bukkits/users/index.html.erb")
+ assert_not File.exist?("app/views/bukkits/users/edit.html.erb")
+ assert_not File.exist?("app/views/bukkits/users/show.html.erb")
+ assert_not File.exist?("app/views/bukkits/users/new.html.erb")
+ assert_not File.exist?("app/views/bukkits/users/_form.html.erb")
+
+ assert_not File.exist?("app/helpers/bukkits/users_helper.rb")
+
+ assert_not File.exist?("app/assets/javascripts/bukkits/users.js")
+ assert_not File.exist?("app/assets/stylesheets/bukkits/users.css")
+ end
+ end
end
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 08b0e34fe2..cc9d3629e9 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -109,6 +109,7 @@ module SharedGeneratorTests
def test_skip_git
run_generator [destination_root, "--skip-git", "--full"]
assert_no_file(".gitignore")
+ assert_no_directory(".git")
end
def test_skip_keeps
diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb
new file mode 100644
index 0000000000..e8e561ec49
--- /dev/null
+++ b/railties/test/generators/system_test_generator_test.rb
@@ -0,0 +1,12 @@
+require "generators/generators_test_helper"
+require "rails/generators/rails/system_test/system_test_generator"
+
+class SystemTestGeneratorTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+ arguments %w(user)
+
+ def test_system_test_skeleton_is_created
+ run_generator
+ assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/
+ end
+end
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 68ba435393..c3c16b6f86 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -200,7 +200,7 @@ class GeneratorsTest < Rails::Generators::TestCase
self.class.class_eval(<<-end_eval, __FILE__, __LINE__ + 1)
class WithOptionsGenerator < Rails::Generators::Base
- class_option :generate, :default => true
+ class_option :generate, default: true, type: :boolean
end
end_eval
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index aa0a06faf1..924503a522 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -22,6 +22,7 @@ require "active_support/core_ext/object/blank"
require "active_support/testing/isolation"
require "active_support/core_ext/kernel/reporting"
require "tmpdir"
+require "rails/secrets"
module TestHelpers
module Paths
@@ -162,7 +163,6 @@ module TestHelpers
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
- require "action_dispatch/middleware/flash"
@app = Class.new(Rails::Application)
@app.config.eager_load = false
diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb
index 7dd91a2465..33b4bc6a3a 100644
--- a/railties/test/rack_logger_test.rb
+++ b/railties/test/rack_logger_test.rb
@@ -20,7 +20,7 @@ module Rails
def development?; false; end
end
- class Subscriber < Struct.new(:starts, :finishes)
+ Subscriber = Struct.new(:starts, :finishes) do
def initialize(starts = [], finishes = [])
super
end
diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb
index 59e79de41a..9f4c5bb025 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -54,7 +54,7 @@ class InfoTest < ActiveSupport::TestCase
end
end
- protected
+ private
def properties
Rails::Info.properties
end
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 70a6cd90b2..52d691b73b 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -890,7 +890,7 @@ YAML
boot_rails
- assert_equal AppTemplate.railtie_namespace, AppTemplate::Engine
+ assert_equal AppTemplate::Engine, AppTemplate.railtie_namespace
end
test "properly reload routes" do
diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb
index 5838d0d7e7..6639e55382 100644
--- a/railties/test/railties/mounted_engine_test.rb
+++ b/railties/test/railties/mounted_engine_test.rb
@@ -141,7 +141,7 @@ module ApplicationTests
end
def engine_asset_path
- render inline: "<%= asset_path 'images/foo.png' %>"
+ render inline: "<%= asset_path 'images/foo.png', skip_pipeline: true %>"
end
end
end
diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb
new file mode 100644
index 0000000000..953408f0b4
--- /dev/null
+++ b/railties/test/secrets_test.rb
@@ -0,0 +1,108 @@
+require "abstract_unit"
+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
+
+ @old_read_encrypted_secrets, Rails::Secrets.read_encrypted_secrets =
+ Rails::Secrets.read_encrypted_secrets, true
+ end
+
+ def teardown
+ Rails::Secrets.read_encrypted_secrets = @old_read_encrypted_secrets
+
+ teardown_app
+ end
+
+ test "setting read to false skips parsing" do
+ Rails::Secrets.read_encrypted_secrets = false
+
+ Dir.chdir(app_path) do
+ assert_equal Hash.new, Rails::Secrets.parse(%w( config/secrets.yml.enc ), env: "production")
+ end
+ end
+
+ test "raises when reading secrets without a key" do
+ run_secrets_generator do
+ FileUtils.rm("config/secrets.yml.key")
+
+ assert_raises Rails::Secrets::MissingKeyError do
+ Rails::Secrets.key
+ end
+ end
+ end
+
+ test "reading with ENV variable" do
+ run_secrets_generator do
+ begin
+ old_key = ENV["RAILS_MASTER_KEY"]
+ ENV["RAILS_MASTER_KEY"] = IO.binread("config/secrets.yml.key").strip
+ FileUtils.rm("config/secrets.yml.key")
+
+ assert_match "production:\n# external_api_key", Rails::Secrets.read
+ ensure
+ ENV["RAILS_MASTER_KEY"] = old_key
+ end
+ end
+ end
+
+ test "reading from key file" do
+ run_secrets_generator do
+ File.binwrite("config/secrets.yml.key", "00112233445566778899aabbccddeeff")
+
+ assert_equal "00112233445566778899aabbccddeeff", Rails::Secrets.key
+ end
+ end
+
+ test "editing" do
+ run_secrets_generator do
+ decrypted_path = nil
+
+ Rails::Secrets.read_for_editing do |tmp_path|
+ decrypted_path = tmp_path
+
+ assert_match(/production:\n# external_api_key/, File.read(tmp_path))
+
+ File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.")
+ end
+
+ assert_not File.exist?(decrypted_path)
+ assert_equal "Empty streets, empty nights. The Downtown Lights.", Rails::Secrets.read
+ end
+ end
+
+ test "merging secrets with encrypted precedence" do
+ run_secrets_generator do
+ File.write("config/secrets.yml", <<-end_of_secrets)
+ test:
+ yeah_yeah: lets-go-walking-down-this-empty-street
+ end_of_secrets
+
+ Rails::Secrets.write(<<-end_of_secrets)
+ test:
+ yeah_yeah: lets-walk-in-the-cool-evening-light
+ end_of_secrets
+
+ Rails.application.config.root = app_path
+ Rails.application.instance_variable_set(:@secrets, nil) # Dance around caching 💃🕺
+ assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah
+ end
+ end
+
+ private
+ def run_secrets_generator
+ Dir.chdir(app_path) do
+ capture(:stdout) do
+ Rails::Generators::EncryptedSecretsGenerator.start
+ end
+
+ yield
+ end
+ end
+end
diff --git a/tasks/release.rb b/tasks/release.rb
index d1717cec52..b021535245 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,11 +1,25 @@
FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable railties )
+FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") }
root = File.expand_path("../../", __FILE__)
version = File.read("#{root}/RAILS_VERSION").strip
tag = "v#{version}"
+gem_version = Gem::Version.new(version)
directory "pkg"
+# This "npm-ifies" the current version number
+# With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its
+# versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively.
+
+# "5.0.1" --> "5.0.1"
+# "5.0.1.1" --> "5.0.1-1" *
+# "5.0.0.rc1" --> "5.0.0-rc1"
+#
+# * This makes it a prerelease. That's bad, but we haven't come up with
+# a better solution at the moment.
+npm_version = version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s }
+
(FRAMEWORKS + ["rails"]).each do |framework|
namespace framework do
gem = "pkg/#{framework}-#{version}.gem"
@@ -43,6 +57,17 @@ directory "pkg"
raise "Could not insert PRE in #{file}" unless $1
File.open(file, "w") { |f| f.write ruby }
+
+ require "json"
+ if File.exist?("#{framework}/package.json") && JSON.parse(File.read("#{framework}/package.json"))["version"] != npm_version
+ Dir.chdir("#{framework}") do
+ if sh "which npm"
+ sh "npm version #{npm_version} --no-git-tag-version"
+ else
+ raise "You must have npm installed to release Rails."
+ end
+ end
+ end
end
task gem => %w(update_versions pkg) do
@@ -61,38 +86,10 @@ directory "pkg"
task push: :build do
sh "gem push #{gem}"
- # When running the release task we usually run build first to check that the gem works properly.
- # NPM will refuse to publish or rebuild the gem if the version is changed when the Rails gem
- # versions are changed. This then causes the gem push to fail. Because of this we need to update
- # the version and publish at the same time.
if File.exist?("#{framework}/package.json")
Dir.chdir("#{framework}") do
- # This "npm-ifies" the current version
- # With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its
- # versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively.
-
- # In essence, the code below runs through all "."s that appear in the version,
- # and checks to see if their index in the version string is greater than or equal to 2,
- # and if so, it will change the "." to a "-".
-
- # Sample version transformations:
- # irb(main):001:0> version = "5.0.1.1"
- # => "5.0.1.1"
- # irb(main):002:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s }
- # => "5.0.1-1"
- # irb(main):003:0> version = "5.0.0.rc1"
- # => "5.0.0.rc1"
- # irb(main):004:0> version.gsub(/\./).with_index { |s, i| i >= 2 ? '-' : s }
- # => "5.0.0-rc1"
- version = version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s }
-
- # Check if npm is installed, and raise an error if not
- if sh "which npm"
- sh "npm version #{version} --no-git-tag-version"
- sh "npm publish"
- else
- raise "You must have npm installed to release Rails."
- end
+ npm_tag = version =~ /[a-z]/ ? "pre" : "latest"
+ sh "npm publish --tag #{npm_tag}"
end
end
end
@@ -104,9 +101,11 @@ namespace :changelog do
(FRAMEWORKS + ["guides"]).each do |fw|
require "date"
fname = File.join fw, "CHANGELOG.md"
+ current_contents = File.read(fname)
- header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n* No changes.\n\n\n"
- contents = header + File.read(fname)
+ header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n"
+ header << "* No changes.\n\n\n" if current_contents =~ /\A##/
+ contents = header + current_contents
File.open(fname, "wb") { |f| f.write contents }
end
end
@@ -143,7 +142,7 @@ namespace :all do
task push: FRAMEWORKS.map { |f| "#{f}:push" } + ["rails:push"]
task :ensure_clean_state do
- unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock'`.strip.empty?
+ unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock\\|package.json\\|version.rb'`.strip.empty?
abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed"
end
@@ -158,14 +157,16 @@ namespace :all do
end
task :commit do
- File.open("pkg/commit_message.txt", "w") do |f|
- f.puts "# Preparing for #{version} release\n"
- f.puts
- f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT"
- end
+ unless `git status -s`.strip.empty?
+ File.open("pkg/commit_message.txt", "w") do |f|
+ f.puts "# Preparing for #{version} release\n"
+ f.puts
+ f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT"
+ end
- sh "git add . && git commit --verbose --template=pkg/commit_message.txt"
- rm_f "pkg/commit_message.txt"
+ sh "git add . && git commit --verbose --template=pkg/commit_message.txt"
+ rm_f "pkg/commit_message.txt"
+ end
end
task :tag do
@@ -173,7 +174,74 @@ namespace :all do
sh "git push --tags"
end
- task prep_release: %w(ensure_clean_state build)
+ task prep_release: %w(ensure_clean_state build bundle commit)
+
+ task release: %w(prep_release tag push)
+end
+
+task :announce do
+ Dir.chdir("pkg/") do
+ if gem_version.segments[2] == 0 || gem_version.segments[3].is_a?(Integer)
+ # Not major releases, and not security releases
+ raise "Only valid for patch releases"
+ end
+
+ sums = "$ shasum -a 256 *-#{version}.gem\n" + `shasum -a 256 *-#{version}.gem`
- task release: %w(ensure_clean_state build bundle commit tag push)
+ puts "Hi everyone,"
+ puts
+
+ puts "I am happy to announce that Rails #{version} has been released."
+ puts
+
+ previous_version = gem_version.segments[0, 3]
+ previous_version[2] -= 1
+ previous_version = previous_version.join(".")
+
+ if version =~ /rc/
+ require "date"
+ future_date = Date.today + 5
+ future_date += 1 while future_date.saturday? || future_date.sunday?
+
+ github_user = `git config github.user`.chomp
+
+ puts <<MSG
+If no regressions are found, expect the final release on #{future_date.strftime('%A, %B %-d, %Y')}.
+If you find one, please open an [issue on GitHub](https://github.com/rails/rails/issues/new)
+#{"and mention me (@#{github_user}) on it, " unless github_user.empty?}so that we can fix it before the final release.
+
+MSG
+ end
+
+ puts <<MSG
+## CHANGES since #{previous_version}
+
+To view the changes for each gem, please read the changelogs on GitHub:
+
+MSG
+ FRAMEWORKS.sort.each do |framework|
+ puts "* [#{FRAMEWORK_NAMES[framework]} CHANGELOG](https://github.com/rails/rails/blob/v#{version}/#{framework}/CHANGELOG.md)"
+ end
+ puts <<MSG
+
+*Full listing*
+
+To see the full list of changes, [check out all the commits on
+GitHub](https://github.com/rails/rails/compare/v#{previous_version}...v#{version}).
+
+## SHA-256
+
+If you'd like to verify that your gem is the same as the one I've uploaded,
+please use these SHA-256 hashes.
+
+Here are the checksums for #{version}:
+
+```
+#{sums}
+```
+
+As always, huge thanks to the many contributors who helped with this release.
+
+MSG
+ end
end
diff --git a/tools/test.rb b/tools/test.rb
index 7819c13ee2..71349a5974 100644
--- a/tools/test.rb
+++ b/tools/test.rb
@@ -4,6 +4,8 @@ require "bundler"
Bundler.setup
require "rails/test_unit/minitest_plugin"
+require "rails/test_unit/line_filtering"
+require "active_support/test_case"
module Rails
# Necessary to get rerun-snippts working.
@@ -12,4 +14,7 @@ module Rails
end
end
+ActiveSupport::TestCase.extend Rails::LineFiltering
Rails::TestUnitReporter.executable = "bin/test"
+Minitest.run_via = :rails
+require "active_support/testing/autorun"
diff --git a/version.rb b/version.rb
index 9c49e0655a..7bacf2e0ba 100644
--- a/version.rb
+++ b/version.rb
@@ -6,7 +6,7 @@ module Rails
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
PRE = "alpha"